Compare commits

...
Author SHA1 Message Date
Dan Helfman 4af31db99c Bump version for release. 2026-06-01 15:52:45 -07:00
Dan Helfman 7cd9e6b361 Add a minimal stand-alone borgmatic binary in addition to the standard one (#1314). 2026-06-01 15:51:01 -07:00
Dan Helfman 4ee057a909 Experimental "browse" action, a console UI for browsing your backups (#1312).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1312
2026-06-01 20:09:24 +00:00
Dan Helfman 3e7e7879bc Remove some default system commands that aren't particular useful for this app. 2026-06-01 12:59:52 -07:00
Dan Helfman d29cfaf227 Use full URL for image. 2026-06-01 12:47:53 -07:00
Dan Helfman 23a8d665d6 Add a screenshot to the browse docs. 2026-06-01 12:44:32 -07:00
Dan Helfman 2a026a4842 Stop using Ruff "preview" rules, as CI has an older version of Ruff. 2026-06-01 12:42:02 -07:00
Dan Helfman 16c8f81a5a Attempted CI test fixes. Also spelling. 2026-06-01 12:24:57 -07:00
Dan Helfman 1670ba2aeb Add missing tests. 2026-06-01 11:57:49 -07:00
Dan Helfman 5accda1a65 Complete tests. 2026-06-01 11:54:28 -07:00
Dan Helfman ec8e52944c More tests! 2026-05-31 20:30:59 -07:00
Dan Helfman 30ca942f76 Improve performance wen loading file previews. 2026-05-31 18:55:56 -07:00
Dan Helfman 9748a0298a Add icon to configuration files list border title. 2026-05-30 23:11:41 -07:00
Dan Helfman dc91f81a39 Fix loading race conditions. 2026-05-30 22:22:06 -07:00
Dan Helfman 64ceebb2c8 Replace cross-thread calls with signals. 2026-05-30 21:12:02 -07:00
Dan Helfman 7439d7cb8f Docstrings. 2026-05-30 16:58:29 -07:00
Dan Helfman f832a361ed More tests. 2026-05-30 13:58:04 -07:00
Dan Helfman 3a4bdfb3c5 Additional tests and docstrings. 2026-05-30 11:27:59 -07:00
Dan Helfman ed955c2a73 Add additional tests. 2026-05-29 19:56:17 -07:00
Dan Helfman 174aa3d522 Additional tests. Also fix a potential race that loses loaded archive paths for display. 2026-05-29 16:59:00 -07:00
Dan Helfman 5a74b3d081 Fix broken test. 2026-05-29 11:57:13 -07:00
Dan Helfman a876ce2058 Yet more tests. 2026-05-29 11:52:02 -07:00
Dan Helfman e82c1bb195 More tests and refactoring. 2026-05-28 20:00:47 -07:00
Dan Helfman 16061f4f6d Fix tests after recent refactoring. 2026-05-28 17:15:13 -07:00
Dan Helfman 59e6e61786 Merge branch 'main' into browse-action 2026-05-28 11:53:23 -07:00
Dan Helfman b7d0f006d6 MariaDB systemd database documentation clarifications (#1303). 2026-05-28 11:50:25 -07:00
Dan Helfman 6869a5d5f2 Additional tests and docstrings. 2026-05-27 22:40:13 -07:00
Dan Helfman 9da6d0f90a Split out panels.py into multiple separate source files. 2026-05-26 23:06:36 -07:00
Dan Helfman ac65e65302 Merge get_archive_path() into get_archive_paths(). 2026-05-26 22:53:04 -07:00
Dan Helfman 8820a1eeab Faster exit for the browse action. 2026-05-26 12:01:55 -07:00
Dan Helfman 3abe025a80 Better performance when bulk populating directory lists. 2026-05-26 11:38:42 -07:00
Dan Helfman b979e0356d Fix display of ".." while loading is still underway in the browse action. 2026-05-25 17:30:06 -07:00
Dan Helfman ea0ed8345a Performance: Switch browse action to list archive files only once per archive. 2026-05-25 16:27:36 -07:00
Dan Helfman 4b8a6d1a34 More browse tests. 2026-05-24 19:22:50 -07:00
Dan Helfman 60201541e0 Add Textual to stand-alone binary to support the browse action. 2026-05-24 10:16:36 -07:00
Dan Helfman bfd79a5500 Refuse to preview named pipes. 2026-05-23 23:05:28 -07:00
Dan Helfman 43e995a4f3 Binary versus text file detection for borgmatic browse. 2026-05-23 22:50:51 -07:00
Dan Helfman 94a2d68198 Switch from a Static to a RichLog for file preview so we can get scrolling. 2026-05-23 19:20:24 -07:00
Dan Helfman c9ba0762f0 Add docstring. 2026-05-23 18:31:24 -07:00
Dan Helfman 13031a4ce4 Upgrade/add test requirements. 2026-05-22 20:03:32 -07:00
Dan Helfman 9a2e853ea7 Merge branch 'main' into browse-action 2026-05-22 20:01:50 -07:00
Dan Helfman b1f42eb233 Upgrade dependencies. 2026-05-22 20:00:40 -07:00
Dan Helfman 7e7c3c273e Update the Apprise monitoring hook's "url" option to support loading credentials with the "{credential ...}" syntax (#1308). 2026-05-22 15:25:42 -07:00
Dan Helfman 704db9c9d7 Add a "--quick-stats" flag and corresponding "quick_statistics" option (#1309). 2026-05-22 14:12:46 -07:00
Dan Helfman 61d86c55b4 Add missing Rich library to test requirements. 2026-05-22 13:44:38 -07:00
Dan Helfman ecedccadee Yet another NEWS wording tweak. 2026-05-22 13:43:44 -07:00
Dan Helfman 006825838f NEWS tweak. 2026-05-22 13:39:45 -07:00
Dan Helfman 1102bf3787 Merge branch 'main' into browse-action 2026-05-22 13:37:44 -07:00
Dan Helfman 220c76b3c0 Formatting. 2026-05-22 13:37:24 -07:00
Dan Helfman 9aea0b1d90 Fix broken tests. 2026-05-22 12:19:34 -07:00
Dan Helfman b6ab326bb4 Switch to PEP 639 license format. 2026-05-22 10:26:12 -07:00
Dan Helfman c4170bb126 Several browse action unit and integration tests. 2026-05-21 23:23:19 -07:00
Dan Helfman 9935d0ef8d Linting and more file refactoring for the browse action. 2026-05-21 17:01:07 -07:00
Dan Helfman ec11e8817d Code formatting. 2026-05-21 16:08:10 -07:00
Dan Helfman 73222e7b5b Factor out make_next_panel() out of browse action widgets into a plain function. 2026-05-21 15:50:34 -07:00
Dan Helfman fd98f4fdd1 Browse action documentation tweaks. 2026-05-21 14:10:49 -07:00
Dan Helfman 16e743d8d9 More browse action option list keys. 2026-05-21 11:41:44 -07:00
Dan Helfman e93fe4cc15 Browse action refactoring into separate files. 2026-05-21 11:33:05 -07:00
Dan Helfman da0698f6e5 Browse action logging improvements. 2026-05-21 10:08:42 -07:00
Dan Helfman 5075c1e539 Skip the browse action's configuration list if there's only one config file. 2026-05-20 23:10:29 -07:00
Dan Helfman 4b4851e78a Add browse action documentation. 2026-05-20 22:57:52 -07:00
Dan Helfman 8e91af6820 Make Textual an optional dependency. 2026-05-20 22:17:19 -07:00
Dan Helfman 9be1698762 Add syntax highlighting to previews. 2026-05-20 21:00:16 -07:00
Dan Helfman 9310b42d9f Add a few more browse action bindings. 2026-05-20 20:16:30 -07:00
Dan Helfman d1d757a5d1 Fix focus on file preview widgets so key bindings for them work in the browse action. 2026-05-20 20:01:03 -07:00
Dan Helfman 3f4a87517b Fix browse action option list highlights. 2026-05-20 19:40:47 -07:00
Dan Helfman a2dcd4747f Get rid of automatic preview panels in the browse action; they're just too slow and annoying in real-world use. 2026-05-20 17:10:00 -07:00
Dan Helfman 0396a89f72 Various browse action improvements. 2026-05-20 16:38:51 -07:00
Dan Helfman f170ef19df Switch to a generalized "carousel" for the borgmatic browse panels. 2026-05-17 16:10:58 -07:00
Dan Helfman 9729809f6f Add the first level of file loading. 2026-05-16 23:04:30 -07:00
Dan Helfman dfb3c0830e Convert browse action UI from a tree to a series of option lists. 2026-05-16 22:06:39 -07:00
Dan Helfman b22a068a60 Put browse action archive files into an actual tree hierarchy. 2026-05-14 23:26:09 -07:00
Dan Helfman 1286ccce46 Browse action MVC refactoring into multiple files. 2026-05-14 22:53:59 -07:00
Dan Helfman e339ce2fd1 Initial work on a browse action. 2026-05-14 22:24:06 -07:00
Dan Helfman aaf5812a3f Cosmetic function argument ordering fix (#1256). 2026-05-12 22:13:05 -07:00
Dan Helfman 47a6691886 Upgrade dependencies. 2026-05-12 15:29:56 -07:00
Dan Helfman 596d59ef60 Fix a race condition in which borgmatic sometimes swallows Borg error output without logging it (#1256). 2026-05-12 15:28:31 -07:00
84 changed files with 3711 additions and 76 deletions
+13 -1
View File
@@ -1,4 +1,6 @@
2.1.6.dev0
2.1.6
* #1256: Fix a race condition in which borgmatic sometimes swallows Borg error output without
logging it.
* #1300: Fix the "source_directories_must_exist" option to support source directories relative to a
"working_directory".
* #1301: Expand the "patterns_from" and "exclude_from" options to support paths containing tildes
@@ -8,6 +10,16 @@
* #1303: For the MariaDB hook, include only a subset of system data when dumping the "mysql" system
database (or "all" databases), so the dump is actually restorable. See the documentation for more
information: https://torsion.org/borgmatic/reference/configuration/data-sources/mariadb/
* #1314: Add a minimal stand-alone borgmatic binary in addition to the standard one. The minimal
binary omits support for the Apprise monitoring hook.
* Add an experimental "browse" action providing a console UI for browsing your backups. See the
documentation for more information:
https://torsion.org/borgmatic/how-to/inspect-your-backups/#browsing-backups
* #1308: Update the Apprise monitoring hook's "url" option to support loading credentials with the
"{credential ...}" syntax. See the documentation for more information:
https://torsion.org/borgmatic/reference/configuration/credentials/
* #1309: Add a "--quick-stats" flag and corresponding "quick_statistics" option for showing only
abbreviated statistics for the "create" action. Borg 1.4.5+ only.
* Update the KeePassXC credential hook to support KeePassXC's secret service integration. See the
documentation for more information:
https://torsion.org/borgmatic/reference/configuration/credentials/keepassxc/
+5 -5
View File
@@ -3,10 +3,10 @@
apprise==1.10.0 # via -r binary_requirements.in
attrs==26.1.0 # via jsonschema, referencing, -r binary_requirements.in
. # via -r binary_requirements.in
certifi==2026.4.22 # via apprise, requests, -r binary_requirements.in
certifi==2026.5.20 # via apprise, requests, -r binary_requirements.in
charset-normalizer==3.4.7 # via requests, -r binary_requirements.in
click==8.3.3 # via apprise, -r binary_requirements.in
idna==3.13 # via requests, -r binary_requirements.in
click==8.4.1 # via apprise, -r binary_requirements.in
idna==3.16 # via requests, -r binary_requirements.in
jsonschema==4.26.0 # via borgmatic, -r binary_requirements.in
jsonschema-specifications==2025.9.1 # via jsonschema, -r binary_requirements.in
markdown==3.10.2 # via apprise, -r binary_requirements.in
@@ -14,8 +14,8 @@ oauthlib==3.3.1 # via requests-oauthlib, -r binary_requirements.in
packaging==26.2 # via borgmatic, -r binary_requirements.in
pyyaml==6.0.3 # via apprise, -r binary_requirements.in
referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r binary_requirements.in
requests==2.33.1 # via apprise, borgmatic, requests-oauthlib, -r binary_requirements.in
requests==2.34.2 # via apprise, borgmatic, requests-oauthlib, -r binary_requirements.in
requests-oauthlib==2.0.0 # via apprise, -r binary_requirements.in
rpds-py==0.30.0 # via jsonschema, referencing, -r binary_requirements.in
ruamel-yaml==0.19.1 # via borgmatic, -r binary_requirements.in
urllib3==2.6.3 # via requests, -r binary_requirements.in
urllib3==2.7.0 # via requests, -r binary_requirements.in
+108
View File
@@ -0,0 +1,108 @@
import signal
import textual.app
import textual.binding
import textual.widgets
import borgmatic.actions.browse.carousel
import borgmatic.actions.browse.configuration_files_list
import borgmatic.actions.browse.logs
import borgmatic.actions.browse.repositories_list
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'),
textual.binding.Binding(
key='c', action='command_palette', description='commands', show=False
),
)
COMMAND_PALETTE_BINDING = 'c'
CSS = '''
.panel {
border: round $primary;
border-title-color: $text-primary;
width: 100%;
height: 100%;
}
#logs {
width: 100%;
height: 50%;
display: none;
}
'''
def __init__(self, configs):
self.configs = configs
super().__init__()
def get_system_commands(self, screen): # pragma: no cover
'''
Remove the screenshot system command because it produces broken screenshots. (Emoji
weirdness, etc.) Also remove minimize and maximize because they don't accomplish much with
out particular layout.
'''
yield from (
command
for command in super().get_system_commands(screen)
if command.title not in {'Screenshot', 'Minimize', 'Maximize'}
)
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.configuration_files_list.Configuration_files_list(
self.configs
)
]
if len(self.configs) > 1
else [
borgmatic.actions.browse.repositories_list.Repositories_list(
next(iter(self.configs.values()))
)
]
)
logs_panel = borgmatic.actions.browse.logs.Logs()
yield logs_panel
yield textual.widgets.Footer()
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):
'''
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'
def exit(self): # pragma: no cover
'''
Exit the application. But first raise a SIGTERM (handled in borgmatic/signals.py) to
encourage a fast exit by killing any ongoing Borg subprocesses.
'''
signal.raise_signal(signal.SIGTERM)
super().exit()
+143
View File
@@ -0,0 +1,143 @@
import argparse
import collections
import enum
import json
import logging
import binaryornot.helpers
import borgmatic.borg.extract
import borgmatic.borg.list
import borgmatic.borg.repo_list
import borgmatic.borg.version
logger = logging.getLogger(__name__)
class Path_type(enum.Enum):
DIRECTORY = 'd'
LINK = 'l'
PIPE = 'p'
FILE = '-'
# A data structure capturing a path stored in a Borg archive.
Archive_path = collections.namedtuple(
'Archive_path',
('path_type', 'file_path', 'link_target'),
)
def get_repository_archives(config, repository):
'''
Given a configuration dict and a repository dict, return a list of the repository's archives,
one dict per archive.
'''
with borgmatic.logger.Log_prefix(repository.get('label', repository['path'])):
logger.info('Listing repository')
repo_list_arguments = argparse.Namespace(
repository=repository['path'],
short=None,
format=None,
json=True,
prefix=None,
match_archives=None,
sort_by=None,
first=None,
last=None,
)
global_arguments = argparse.Namespace()
local_path = config.get('local_path', 'borg')
remote_path = config.get('remote_path')
local_borg_version = borgmatic.borg.version.local_borg_version(config, local_path)
return json.loads(
borgmatic.borg.repo_list.list_repository(
repository['path'],
config,
local_borg_version,
repo_list_arguments,
global_arguments,
local_path,
remote_path,
)
)
def get_archive_paths(config, repository, archive_name):
'''
Given a configuration dict, a repository dict, and an archive name in that repository, get a
generator of files, directories, symlinks, etc. found in the archive, each as an Archive_path
instance.
'''
with borgmatic.logger.Log_prefix(repository.get('label', repository['path'])):
logger.info(f'Listing archive {archive_name}')
global_arguments = argparse.Namespace()
local_path = config.get('local_path', 'borg')
remote_path = config.get('remote_path')
local_borg_version = borgmatic.borg.version.local_borg_version(config, local_path)
return (
Archive_path(
path_data['type'],
path_data['path'],
path_data.get('linktarget'),
)
for path_data in borgmatic.borg.list.capture_archive_listing(
repository['path'],
archive_name,
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
)
READLINES_HINT_BYTES = 100000
TRUNCATION_MESSAGE = '[... truncated for display ...]'
def get_archive_file_content(config, repository, archive_name, file_path):
'''
Given a configuration dict, a repository dict, an archive name in that repository, and a file
path in that archive, return the file's contents or None if the file can't be loaded, e.g.
because it's binary or can't be decoded.
If the file is too large, then truncate the returned content.
'''
with borgmatic.logger.Log_prefix(repository.get('label', repository['path'])):
logger.info(f'Getting archive content of file {file_path}')
local_path = config.get('local_path', 'borg')
remote_path = config.get('remote_path')
lines = borgmatic.borg.extract.extract_archive(
dry_run=False,
repository=repository['path'],
archive=archive_name,
paths=(file_path,),
config=config,
local_borg_version=borgmatic.borg.version.local_borg_version(config, local_path),
global_arguments=argparse.Namespace(),
local_path=local_path,
remote_path=remote_path,
destination_path=None,
strip_components=None,
extract_to_stdout=True,
).stdout.readlines(READLINES_HINT_BYTES)
content = b''.join(lines)
if binaryornot.helpers.is_binary_string(content):
return None
try:
return (
content.decode()
if len(content) < READLINES_HINT_BYTES
else f'{content.decode()}\n{TRUNCATION_MESSAGE}'
)
except UnicodeDecodeError:
return None
+89
View File
@@ -0,0 +1,89 @@
import textual.widgets
import borgmatic.actions.browse.bindings
import borgmatic.actions.browse.loading
import borgmatic.actions.browse.workers
class Archives_list(textual.widgets.OptionList):
'''
A widget for selecting a single Borg archive from among the archives in a repository. The item
selection event is handled in a Carousel instance, the parent widget of an Archives_list.
'''
BINDINGS = borgmatic.actions.browse.bindings.OPTION_LIST_BINDINGS
def __init__(self, config, repository):
'''
Given a configuration dict and a repository dict, prepare to load the archives from the
repository for eventual display in this widget. Actual loading kicks off in on_mount()
below.
'''
self.config = config
self.repository = repository
super().__init__(classes='panel')
self.border_title = '📚 archives'
self.highlighted_option_changed = False
self.archive_loaded = borgmatic.actions.browse.workers.Archive_loaded(
self, 'archive loaded'
)
self.loading_timer = borgmatic.actions.browse.loading.add_inline_loading_indicator(self)
def on_mount(self):
'''
When this widget gets mounted in the DOM, subscribe to archive loaded events so that we can
find out about archives as they load. Also start loading archives from the repository.
Loading is started *after* subscribing to the archive loaded signal so that there's not a
gap where we might miss out on signal publishes.
'''
self.archive_loaded.subscribe(self, self.on_archive_loaded)
borgmatic.actions.browse.workers.add_repository_archives(
self.app,
archive_loaded=self.archive_loaded,
config=self.config,
repository=self.repository,
)
def on_archive_loaded(self, archive_name):
'''
When an archive loads, add it as an option to this archives list. But if we get a
signal that all path loading is complete, stop and remove our loading indicator.
'''
if archive_name is borgmatic.actions.browse.workers.LOADING_DONE:
self.loading_timer.stop()
self.remove_option('loading-indicator')
return
label_pieces = (
(archive_name, '[dim](latest)[/dim]') if len(self.options) == 1 else (archive_name,)
)
highlighted_option = self.highlighted_option
loading_indicator = self.get_option('loading-indicator')
self.remove_option('loading-indicator')
self.add_options(
(
textual.widgets.option_list.Option(' '.join(label_pieces), id=archive_name),
loading_indicator,
),
)
# Retain the highlighted option position even as other options load around it.
self.highlighted = (
self.get_option_index(highlighted_option.id)
if highlighted_option and self.highlighted_option_changed
else 0
)
def on_option_list_option_highlighted(self, event):
'''
When the highlighted option changes, record that fact. This flag is consumed in
borgmatic.actions.browse.workers.add_repository_archives() in order to retain the
highlighted option even as other options load around it.
'''
if self.highlighted not in {None, 0}:
self.highlighted_option_changed = True
+22
View File
@@ -0,0 +1,22 @@
import textual.binding
import textual.widgets
OPTION_LIST_BINDINGS = (
*textual.widgets.OptionList.BINDINGS,
textual.binding.Binding(
key='up,k', action='cursor_up', description='scroll up', show=True, priority=True
),
textual.binding.Binding(
key='down,j', action='cursor_down', description='scroll down', show=True, priority=True
),
textual.binding.Binding(
key='pageup', action='page_up', description='page up', show=True, priority=True
),
textual.binding.Binding(
key='pagedown', action='page_down', description='page down', show=True, priority=True
),
textual.binding.Binding(
key='enter', action='select', description='select', show=True, priority=True
),
textual.binding.Binding(key='right,l', action='select', description='select', show=False),
)
+153
View File
@@ -0,0 +1,153 @@
import os
import textual.binding
import textual.containers
import borgmatic.actions.browse.archive
import borgmatic.actions.browse.archives_list
import borgmatic.actions.browse.configuration_files_list
import borgmatic.actions.browse.directory_list
import borgmatic.actions.browse.file_preview
import borgmatic.actions.browse.icons
import borgmatic.actions.browse.repositories_list
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.configuration_files_list.Configuration_files_list
):
return borgmatic.actions.browse.repositories_list.Repositories_list(
config=focused_panel.configs[option_id]
)
if isinstance(focused_panel, borgmatic.actions.browse.repositories_list.Repositories_list):
return borgmatic.actions.browse.archives_list.Archives_list(
config=focused_panel.config, repository=focused_panel.repositories[option_id]
)
if isinstance(focused_panel, borgmatic.actions.browse.archives_list.Archives_list):
return borgmatic.actions.browse.directory_list.Directory_list(
config=focused_panel.config, repository=focused_panel.repository, archive_name=option_id
)
if isinstance(focused_panel, borgmatic.actions.browse.directory_list.Directory_list):
option = focused_panel.get_option(option_id)
if option.prompt.startswith(
borgmatic.actions.browse.icons.PATH_TYPE_ICONS[
borgmatic.actions.browse.archive.Path_type.DIRECTORY.value
]
):
return borgmatic.actions.browse.directory_list.Directory_list(
focused_panel.config,
focused_panel.repository,
focused_panel.archive_name,
path_loaded=focused_panel.path_loaded,
path_components=(*focused_panel.path_components, option_id),
)
if option.prompt.startswith(
borgmatic.actions.browse.icons.PATH_TYPE_ICONS[
borgmatic.actions.browse.archive.Path_type.FILE.value
]
):
return borgmatic.actions.browse.file_preview.File_preview(
focused_panel.config,
focused_panel.repository,
focused_panel.archive_name,
file_path=os.path.sep.join((*focused_panel.path_components, option_id)),
)
return None
class Carousel(textual.containers.Horizontal):
BINDINGS = (
textual.binding.Binding(
key='left,h', action='previous', description='previous', priority=True
),
)
def __init__(self, panels):
self.panels = panels
self.focused_panel = panels[0]
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()
def action_previous(self):
'''
Make the previous panel into the focused panel.
'''
previous_panel_index = self.panels.index(self.focused_panel) - 1
if previous_panel_index < 0:
return
self.focused_panel.styles.display = 'none'
self.focused_panel = self.panels[previous_panel_index]
self.focused_panel.styles.display = 'block'
self.focused_panel.focus()
def action_next(self, option_id):
'''
Hide the current focused panel and create the next one.
'''
next_panel_index = self.panels.index(self.focused_panel) + 1
if next_panel_index < len(self.panels):
next_panel = self.panels[next_panel_index]
next_panel.styles.display = 'block'
else:
next_panel = make_next_panel(self.focused_panel, option_id)
if next_panel is None:
self.notify('Cannot display this content', severity='warning')
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.mount(self.focused_panel)
def on_option_list_option_highlighted(self, event):
'''
The highlighted option has changed, so truncate any next panels.
'''
next_panel_index = self.panels.index(self.focused_panel) + 1
del self.panels[next_panel_index:]
def on_option_list_option_selected(self, event):
'''
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'
): # pragma: no cover
return
if event.option_id == '..':
self.action_previous()
else:
self.action_next(event.option_id)
@@ -0,0 +1,33 @@
import os
import textual.widgets
import borgmatic.actions.browse.bindings
class Configuration_files_list(textual.widgets.OptionList):
'''
A widget for selecting a single borgmatic configuration file from among available configuration
files. The item selection event is handled in a Carousel instance, the parent widget of an
Configuration_files_list.
'''
BINDINGS = borgmatic.actions.browse.bindings.OPTION_LIST_BINDINGS
def __init__(self, configs):
'''
Given a dict mapping from configuration path to corresponding configuration dict, add each
configuration path as an option to this widget.
'''
self.configs = configs
home_directory = os.path.expanduser('~')
super().__init__(
*(
textual.widgets.option_list.Option(unexpanded_path, id=config_path)
for config_path in configs
for unexpanded_path in (config_path.replace(home_directory, '~'),)
),
classes='panel',
)
self.border_title = '📄 configuration files'
+211
View File
@@ -0,0 +1,211 @@
import os
import textual.widgets
import borgmatic.actions.browse.bindings
import borgmatic.actions.browse.icons
import borgmatic.actions.browse.loading
import borgmatic.actions.browse.workers
def get_relative_archive_path_components(archive_path, current_directory_path_components):
'''
Given an Archive_path instance and a tuple of path components for the currently browsed
directory, get the path components as a tuple for the archive path relative to that directory.
For instance, given an archive path with a path of 'foo/bar/baz/quux.txt' and current
directory path components of ('foo', 'bar'), return ('baz', 'quux.txt').
If the archive path is not actually relative to the current directory, return None.
'''
archive_path_components = tuple(archive_path.file_path.split(os.path.sep))
if not current_directory_path_components:
return archive_path_components
# If the loaded path doesn't match this directory list's own path, then we don't care about
# it for purposes of displaying this particular directory.
if (
tuple(archive_path_components[: len(current_directory_path_components)])
!= current_directory_path_components
):
return None
# Strip off the portion of the archive path that matches the directory list's own path.
return archive_path_components[len(current_directory_path_components) :]
def make_directory_list_option(archive_path, relative_path_components):
'''
Given an Archive_path instance and a tuple of relative path components for it, make a
textual.widgets.option_list.Option for the path. Use an the icon based on whether this looks
like a terminal filename or a directory.
'''
pieces = (
borgmatic.actions.browse.icons.PATH_TYPE_ICONS.get(
archive_path.path_type if len(relative_path_components) == 1 else 'd', ''
),
relative_path_components[0],
) + (('', archive_path.link_target) if archive_path.link_target else ())
return textual.widgets.option_list.Option(
prompt=' '.join(pieces), id=relative_path_components[0]
)
def add_archive_paths(
directory_list,
config,
repository,
archive_name,
archive_paths,
):
'''
Given a DirectoryList instance, a configuration dict, a repository dict, an archive name, and a
sequence of ArchivePath instances, add the paths to the directory list as options, sorting and
deduplicating the resulting directory list's options.
After all of this reshuffling, make sure the original highlighted option remains highlighted.
'''
highlighted_option = directory_list.highlighted_option
original_options_count = len(directory_list.options)
sorted_options = sorted(
(
*directory_list.options,
*(
make_directory_list_option(archive_path, relative_path_components)
for archive_path in archive_paths
for relative_path_components in (
get_relative_archive_path_components(
archive_path,
directory_list.path_components,
),
)
if relative_path_components
if relative_path_components[0] not in directory_list._id_to_option
),
),
# The loading indicator "option" always goes to the bottom.
key=lambda option: ((option.id == 'loading-indicator'), option.prompt),
)
# If there aren't actually any options to add (due to deduplication), bail.
if len(sorted_options) == original_options_count:
return
# Retain the highlighted option position even as other options load around it.
directory_list.set_options(sorted_options)
directory_list.highlighted = (
directory_list.get_option_index(highlighted_option.id)
if highlighted_option and directory_list.highlighted_option_changed
else 0
)
class Directory_list(textual.widgets.OptionList):
'''
A widget for selecting a path from among the contents of a particular directory in a Borg
archive. The item selection event is handled in a Carousel instance, the parent widget of a
Directory_list.
'''
BINDINGS = borgmatic.actions.browse.bindings.OPTION_LIST_BINDINGS
def __init__(self, config, repository, archive_name, path_loaded=None, path_components=None):
'''
Given a configuration dict, a repository dict, an archive name, an optional
Archive_path_loaded instance for signalling new paths as they load, and an optional tuple of
path components indicating this directory's position in the backed up filesystem, prepare to
load paths from the archive for eventual display in this widget. Actual loading kicks off in
on_mount() below.
'''
self.config = config
self.repository = repository
self.archive_name = archive_name
self.path_components = path_components or ()
self.highlighted_option_changed = False
super().__init__(classes='panel')
self.border_title = ' '.join(
(
'📁',
os.path.sep.join(self.path_components)
if self.path_components
else f'{archive_name}',
)
)
if self.path_components:
self.add_option(
textual.widgets.option_list.Option(
'📁 ..',
id='..',
),
)
self.path_loaded = path_loaded or borgmatic.actions.browse.workers.Archive_path_loaded(
self, 'archive path loaded'
)
if not self.path_loaded.complete:
self.timer = borgmatic.actions.browse.loading.add_inline_loading_indicator(self)
def on_mount(self):
'''
When this widget gets mounted in the DOM, subscribe to path loaded events so that we can
find out about relevant archive paths as they load. And if this is a root directory list,
start loading paths from the archive. If this is a non-root directory list, add any already
loaded archive paths to this widget as options.
Loading is started *after* subscribing to path loaded signals so that there's not a gap
where we might miss out on any paths.
'''
self.path_loaded.subscribe(self, self.on_archive_path_loaded)
if self.path_components:
add_archive_paths(
directory_list=self,
config=self.config,
repository=self.repository,
archive_name=self.archive_name,
archive_paths=borgmatic.actions.browse.workers.get_paths(
self.path_loaded.path_hierarchy, self.path_components
),
)
else:
borgmatic.actions.browse.workers.load_archive_paths(
self.app,
path_loaded=self.path_loaded,
config=self.config,
repository=self.repository,
archive_name=self.archive_name,
)
def on_archive_path_loaded(self, data):
'''
When an archive path loads, add it as an option to this directory list. But if we get a
signal that all path loading is complete, stop and remove our loading indicator.
'''
if data is borgmatic.actions.browse.workers.LOADING_DONE:
self.timer.stop()
self.remove_option('loading-indicator')
return
add_archive_paths(
directory_list=self,
config=self.config,
repository=self.repository,
archive_name=self.archive_name,
archive_paths=(data,),
)
def on_option_list_option_highlighted(self, event):
'''
When the highlighted option changes, record that fact. This flag is consumed in
add_archive_paths() in order to retain the highlighted option even as other options load
around it.
'''
if self.highlighted not in {None, 0}:
self.highlighted_option_changed = True
+87
View File
@@ -0,0 +1,87 @@
import logging
import rich.syntax
import textual.binding
import textual.widgets
import borgmatic.actions.browse.loading
import borgmatic.actions.browse.workers
logger = logging.getLogger('__name__')
class File_preview(textual.widgets.RichLog):
'''
A widget for extracting and previewing the contents of a file stored in a Borg archive.
'''
BINDINGS = (
*textual.widgets.RichLog.BINDINGS,
textual.binding.Binding(
key='up,k', action='scroll_up', description='scroll up', show=True, priority=True
),
textual.binding.Binding(
key='down,j', action='scroll_down', description='scroll down', show=True, priority=True
),
textual.binding.Binding(
key='pageup', action='page_up', description='page up', show=True, priority=True
),
textual.binding.Binding(
key='pagedown', action='page_down', description='page down', show=True, priority=True
),
)
def __init__(self, config, repository, archive_name, file_path):
'''
Given a configuration dict, a repository dict, an archive name, and the path of a file in
the archive, prepare to load the file's contents for eventual display in this widget. Actual
loading kicks off in on_mount() below.
'''
self.config = config
self.repository = repository
self.archive_name = archive_name
self.file_path = file_path
super().__init__(classes='panel')
self.border_title = f'📄 {self.file_path} preview'
self.auto_scroll = False
self.file_preview_loaded = borgmatic.actions.browse.workers.File_preview_loaded(
self, 'file preview loaded'
)
self.loading_timer = borgmatic.actions.browse.loading.add_inline_loading_indicator(self)
def on_mount(self):
'''
When this widget gets mounted in the DOM, subscribe to archive loaded events so that we can
find out about archives as they load. Also start loading file contents from the archive.
Loading is started *after* subscribing to the file preview loaded signal so that there's not
a gap where we might miss out on signal publishes.
'''
self.file_preview_loaded.subscribe(self, self.on_file_preview_loaded)
borgmatic.actions.browse.workers.load_file_preview(
self.app,
file_preview_loaded=self.file_preview_loaded,
config=self.config,
repository=self.repository,
archive_name=self.archive_name,
file_path=self.file_path,
)
def on_file_preview_loaded(self, file_contents):
'''
When a file loads, write its contents (syntax highlighted) to this file preview widget.
'''
self.loading_timer.stop()
self.clear()
if file_contents is None:
self.write('Cannot display a preview for this file')
else:
# Only pass the file path and not its contents to guess_lexer(). Passing the contents is
# more accurate, but also much slower.
self.write(
rich.syntax.Syntax(file_contents, rich.syntax.Syntax.guess_lexer(self.file_path))
)
+8
View File
@@ -0,0 +1,8 @@
import borgmatic.actions.browse.archive
PATH_TYPE_ICONS = {
borgmatic.actions.browse.archive.Path_type.DIRECTORY.value: '📁',
borgmatic.actions.browse.archive.Path_type.LINK.value: '🔗',
borgmatic.actions.browse.archive.Path_type.PIPE.value: '🚰',
borgmatic.actions.browse.archive.Path_type.FILE.value: '📄',
}
+54
View File
@@ -0,0 +1,54 @@
import contextlib
import functools
import logging
import textual.widgets
import textual.widgets.option_list
LOADING_DOT_INTERVAL_SECONDS = 0.3
logger = logging.getLogger('__name__')
def update_inline_loading_indicator(widget):
'''
Given a textual.widgets.OptionList or a textual.widgets.RichLog instance, animate the existing
loading indicator inside it.
'''
if isinstance(widget, textual.widgets.OptionList):
with contextlib.suppress(textual.widgets.option_list.OptionDoesNotExist):
widget.replace_option_prompt(
'loading-indicator',
(str(widget.get_option('loading-indicator').prompt) + '.').replace('....', ''),
)
elif isinstance(widget, textual.widgets.RichLog):
with contextlib.suppress(IndexError):
loading_message = str(widget.lines[0].text)
widget.clear()
widget.write((loading_message + '.').replace('....', ''))
else:
raise ValueError(f'Unsupported widget type: {type(widget)}')
LOADING_MESSAGE = '⏳ loading...'
def add_inline_loading_indicator(widget):
'''
Given a textual.widgets.OptionList or a textual.widgets.RichLog instance, add a loading
indicator to it.
'''
if isinstance(widget, textual.widgets.OptionList):
loading_option = textual.widgets.option_list.Option(LOADING_MESSAGE, id='loading-indicator')
widget.add_option(loading_option)
widget.highlighted = None
elif isinstance(widget, textual.widgets.RichLog):
widget.write(LOADING_MESSAGE)
else:
raise ValueError(f'Unsupported widget type: {type(widget)}')
return widget.set_interval(
LOADING_DOT_INTERVAL_SECONDS,
functools.partial(update_inline_loading_indicator, widget),
)
+102
View File
@@ -0,0 +1,102 @@
import contextlib
import logging
import textual._context
import textual.widgets
import textual.worker
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__(
'{prefix}{message}',
*args,
style='{',
**kwargs,
)
def format(self, record):
'''
Given a log record, format it with Rich-compatibe color markup corresponding to its log
level.
'''
borgmatic.logger.add_custom_log_levels()
color = {
logging.CRITICAL: 'bright_red',
logging.ERROR: 'bright_red',
logging.WARNING: 'bright_yellow',
logging.ANSWER: 'bright_magenta',
logging.INFO: 'bright_green',
logging.DEBUG: 'bright_cyan',
}.get(record.levelno)
record.prefix = f'{self.prefix}: ' if self.prefix else ''
return f'[{color}]{super().format(record)}[/{color}]'
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:
textual.worker.get_current_worker()
self.logs_widget.app.call_from_thread(self.logs_widget.write, message)
except (RuntimeError, textual.worker.NoActiveWorker):
with contextlib.suppress(textual._context.NoActiveAppError):
self.logs_widget.write(message)
def log_to_widget(logs_widget):
'''
Given a Textual RichLog logs widget, add a log handler and formatter that logs to it. Also
remove the default borgmatic console log handler so it doesn't try to log all over our UI.
'''
handler = Browse_log_handler(logs_widget)
handler.setFormatter(Rich_color_formatter())
logger = logging.getLogger()
logger.setLevel(min(handler.level for handler in logger.handlers))
logger.addHandler(handler)
with contextlib.suppress(StopIteration):
console_handler = next(
handler
for handler in logging.getLogger().handlers
if isinstance(handler, borgmatic.logger.Multi_stream_handler)
)
logger.removeHandler(console_handler)
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'
@@ -0,0 +1,30 @@
import textual.widgets
import borgmatic.actions.browse.bindings
class Repositories_list(textual.widgets.OptionList):
'''
A widget for selecting a single Borg repository from among the repositories in a borgmatic
configuration file. The item selection event is handled in a Carousel instance, the parent
widget of a Repositories_list.
'''
BINDINGS = borgmatic.actions.browse.bindings.OPTION_LIST_BINDINGS
def __init__(self, config):
'''
Given a configuration dict, populate the repositories in this widget.
'''
self.config = config
self.repositories = config['repositories']
super().__init__(
*(
textual.widgets.option_list.Option(label, id=index)
for index, repository in enumerate(self.repositories)
for label in (repository.get('label', repository.get('path')),)
),
classes='panel',
)
self.border_title = '📦 repositories'
+29
View File
@@ -0,0 +1,29 @@
import logging
def run_browse(
diff_arguments,
global_arguments,
configs,
):
'''
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
logging.getLogger('asyncio').setLevel(logging.WARNING)
try:
import textual # noqa: F401, PLC0415
except ImportError: # pragma: no cover
raise ValueError(
'Unable to import the Textual library for the browse action; try installing "borgmatic[browse]"'
)
import borgmatic.actions.browse.app # noqa: PLC0415
app = borgmatic.actions.browse.app.Browse_app(configs)
app.run()
+192
View File
@@ -0,0 +1,192 @@
import logging
import os
import textual
import textual.signal
import borgmatic.actions.browse.archive
logger = logging.getLogger('__name__')
LOADING_DONE = object()
class Archive_loaded(textual.signal.Signal):
'''
A signal that publishes when each subsequent archive is loaded from a repository, intended for
consumption in widgets that display archives as they are loaded. This signal also publishes
when loading is complete.
Each subscribed callback call includes the archive as an archive name string. Given the lack of
other identifying information (configuration file, repository), there should be a separate
Archive_loaded instance per repository.
'''
@textual.work(thread=True)
def add_repository_archives(browse_app, archive_loaded, config, repository):
'''
Given a running Browse_app instance, an Archive_loaded instance, a configuration dict, and a
repository dict, load a list of the archives from the repository and add them as options in the
archives list. Reverse the order so the most recent archive is first.
This function runs in a separate thread from the main UI. When loading is complete, publish a
loading done signal.
'''
archives_data = borgmatic.actions.browse.archive.get_repository_archives(config, repository)
# Reverse the archives, so the common case of accessing the latest archive is easy because it's
# at the top.
for archive in reversed(archives_data['archives']):
archive_loaded.publish(archive['archive'])
archive_loaded.publish(LOADING_DONE)
def record_path(archive_path, hierarchy, path_components):
'''
Given an Archive_path instance, a dict capturing a filesystem hierarchy of paths, and a tuple of
path components for the archive path, set the archive path into the hierarchy data structure.
For instance, if given an archive path and path components representing "foo/bar/baz.txt",
produce a hierarchy that looks like:
{'foo': {'bar': {'baz.txt': Archive_path('-', 'foo/bar/baz.txt', '')}}}
Note that the hierarchy is modified in place, so any existing paths there are retained.
'''
if len(path_components) == 1:
hierarchy[path_components[0]] = {} if archive_path.path_type == 'd' else archive_path
return
record_path(archive_path, hierarchy.setdefault(path_components[0], {}), path_components[1:])
def get_paths(hierarchy, path_components, full_path_components=None):
'''
Given a dict capturing a filesystem hierarchy of paths (or a subset thereof), a tuple of path
components for a directory path relative to the hierarchy root, and an optional tuple of
*absolute* path components for the same path (if different), return a generator of the
contained file and directory Archive_path instances from the hierarchy.
For instance, given the following hierarchy:
{'foo': {'bar': {'baz.txt': Archive_path('-', 'foo/bar/baz.txt', ''), 'quux': {}}}}
... and path components of ('foo', 'bar'), return a generator with the following:
* Archive_path('-', 'foo/bar/baz.txt', '')
* Archive_path('d', 'foo/bar/quux', '')
The given absolute path components are use to construct directory paths like that last archive
path.
'''
if full_path_components is None:
full_path_components = path_components
if len(path_components) == 1:
try:
return (
archive_path
if isinstance(archive_path, borgmatic.actions.browse.archive.Archive_path)
else borgmatic.actions.browse.archive.Archive_path(
'd', os.path.join(*full_path_components, component), ''
)
for component, archive_path in hierarchy[path_components[0]].items()
)
except KeyError:
raise ValueError(f'Unknown file or directory: {path_components[0]}')
try:
return get_paths(hierarchy[path_components[0]], path_components[1:], full_path_components)
except KeyError:
raise ValueError(f'Unknown directory: {path_components[0]}')
class Archive_path_loaded(textual.signal.Signal):
'''
A signal that publishes when each subsequent path is loaded from an archive, intended for
consumption in widgets that display paths as they are loaded. This signal also tracks the
complete filesystem hierarchy seen thus far, so new widgets that get created after loading has
started can "catch up" with existing known paths. Lastly, this signal publishes and tracks when
loading is complete.
Each subscrided callback call includes the loaded path as an Archive_path instance. There is
intended to be a separate Archive_path_loaded instance per archive, but that instance should be
shared among several different widgets for the same archive for performance reasons.
'''
def __init__(self, owner, name):
self.path_hierarchy = {}
self.complete = False
super().__init__(owner, name)
def publish(self, archive_path):
'''
Publish the given archive path to subscribers and record its path locally. But if the
archive path is actually LOADING_DONE, then record loading as complete.
'''
super().publish(archive_path)
if archive_path is LOADING_DONE:
self.complete = True
else:
record_path(
archive_path, self.path_hierarchy, archive_path.file_path.split(os.path.sep)
)
@textual.work(thread=True)
def load_archive_paths(browse_app, path_loaded, config, repository, archive_name):
'''
Given a running Browse_app instance, an Archive_path_loaded instance, a configuration dict, a
repository dict, and an archive name, load the paths in this archive and publish each one via
the Archive_path_loaded signal, so interested widgets can subscribe. Also send a "loading done"
signal when loading completes.
This function runs in a separate thread from the main UI. When loading is complete, publish a
loading done signal.
'''
for archive_path in borgmatic.actions.browse.archive.get_archive_paths(
config, repository, archive_name
):
path_loaded.publish(archive_path)
path_loaded.publish(LOADING_DONE)
class File_preview_loaded(textual.signal.Signal):
'''
A signal that publishes when file contents are loaded from an archive, intended for consumption
in widgets that display loaded files. This signal also publishes when loading is complete.
Each published callback includes a the file's contents as a string. Given the lack of other
identifying information (configuration file, repository, archive), there should be a separate
Archive_loaded instance per previewed file.
'''
@textual.work(thread=True)
def load_file_preview(
browse_app,
file_preview_loaded,
config,
repository,
archive_name,
file_path,
):
'''
Given a running Browse_app instance, a File_preview_loaded instance, a configuration dict, a
repository dict, an archive name, and the path of a file in that archive, load the contents of
the file and write it into the given file preview widget.
This function runs in a separate thread from the main UI. When loading is complete, publish a
loading done signal.
'''
file_contents = borgmatic.actions.browse.archive.get_archive_file_content(
config, repository, archive_name, file_path
)
file_preview_loaded.publish(file_contents)
+8 -1
View File
@@ -374,7 +374,9 @@ def create_archive(
if json:
output_log_level = None
elif config.get('list_details') or (config.get('statistics') and not dry_run):
elif config.get('list_details') or (
(config.get('statistics') or config.get('quick_statistics')) and not dry_run
):
output_log_level = logging.ANSWER
else:
output_log_level = logging.INFO
@@ -386,6 +388,11 @@ def create_archive(
create_flags += (
(('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
+ (('--stats',) if config.get('statistics') and not json and not dry_run else ())
+ (
('--quick-stats',)
if config.get('quick_statistics') and not json and not dry_run
else ()
)
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
+ (('--progress',) if config.get('progress') else ())
+ (('--json',) if json else ())
+3 -3
View File
@@ -115,10 +115,10 @@ def capture_archive_listing(
Given a local or remote repository path, an archive name, a configuration dict, the local Borg
version, global arguments as an argparse.Namespace, the archive paths (or Borg patterns) in
which to list files, the Borg path format indicating keys to include in the output, and local
and remote Borg paths, capture the output of listing that archive and return it as a sequence of
dicts, one per path.
and remote Borg paths, capture the output of listing that archive and return it as a generator
of dicts, one per path.
'''
return tuple(
return (
json.loads(entry)
for entry in execute_command_and_capture_output(
make_list_command(
+27 -6
View File
@@ -31,9 +31,10 @@ ACTION_ALIASES = {
'transfer': [],
'break-lock': [],
'key': [],
'borg': [],
'recreate': [],
'diff': [],
'browse': [],
'borg': [],
}
@@ -324,7 +325,7 @@ def make_argument_description(schema, flag_name):
' To specify a different list element, replace the "[0]" with another array index ("[1]", "[2]", etc.).',
)
if example and schema_type in ('array', 'object'): # noqa: PLR6201
if example and schema_type in ('array', 'object'):
example_buffer = io.StringIO()
yaml = ruamel.yaml.YAML(typ='safe')
yaml.default_flow_style = True
@@ -896,6 +897,13 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
action='store_true',
help='Display statistics of archive',
)
create_group.add_argument(
'--quick-stats',
dest='quick_statistics',
default=None,
action='store_true',
help='Display statistics of archive, skipping repository-wide "All archives" and chunk index statistics [Borg 1.4.5+ only]',
)
create_group.add_argument(
'--list',
'--files',
@@ -1997,8 +2005,9 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
diff_parser = action_parsers.add_parser(
'diff',
aliases=ACTION_ALIASES['diff'],
help='This command finds differences (file contents, user/group/mode) between archives',
description='This command finds differences (file contents, user/group/mode) between archives',
help='Find differences (file contents, user/group/mode) between archives',
description='Find differences (file contents, user/group/mode) between archives',
add_help=False,
)
diff_group = diff_parser.add_argument_group('diff arguments')
diff_group.add_argument(
@@ -2035,6 +2044,18 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
action='store_true',
help='Run the diff according to borgmatic configured patterns (ie do not diff entire archives)',
)
diff_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
browse_parser = action_parsers.add_parser(
'browse',
aliases=ACTION_ALIASES['browse'],
help='Browse repositories, archives, and files in a console UI',
description='Browse repositories, archives, and files in a console UI',
add_help=False,
)
browse_group = browse_parser.add_argument_group('browse arguments')
browse_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
borg_parser = action_parsers.add_parser(
'borg',
aliases=ACTION_ALIASES['borg'],
@@ -2107,7 +2128,7 @@ def parse_arguments(schema, *unparsed_arguments):
)
if (
('list' in arguments and 'repo-info' in arguments and arguments['list'].json) # noqa: PLR0916
('list' in arguments and 'repo-info' in arguments and arguments['list'].json)
or ('list' in arguments and 'info' in arguments and arguments['list'].json)
or ('repo-info' in arguments and 'info' in arguments and arguments['repo-info'].json)
):
@@ -2125,7 +2146,7 @@ def parse_arguments(schema, *unparsed_arguments):
'With the repo-list action, only one of --prefix or --match-archives flags can be used.',
)
if 'info' in arguments and ( # noqa: PLR0916
if 'info' in arguments and (
(arguments['info'].archive and arguments['info'].prefix)
or (arguments['info'].archive and arguments['info'].match_archives)
or (arguments['info'].prefix and arguments['info'].match_archives)
+10 -2
View File
@@ -12,6 +12,7 @@ import ruamel.yaml
import borgmatic.actions.borg
import borgmatic.actions.break_lock
import borgmatic.actions.browse.run
import borgmatic.actions.change_passphrase
import borgmatic.actions.check
import borgmatic.actions.compact
@@ -211,7 +212,7 @@ def run_configuration(config_filename, config, config_paths, arguments): # noqa
f"Skipping {'/'.join(skip_actions)} action{'s' if len(skip_actions) > 1 else ''} due to configured skip_actions",
)
try: # noqa: PLR1702
try:
with (
Monitoring_hooks(config_filename, config, arguments, global_arguments),
borgmatic.hooks.command.Before_after_hooks(
@@ -825,8 +826,8 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
try:
if 'bootstrap' in arguments:
try:
# No configuration file is needed for bootstrap.
local_borg_version = borg_version.local_borg_version(
# No configuration file is needed for bootstrap.
{},
arguments['bootstrap'].local_path,
)
@@ -897,6 +898,13 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
return
if 'browse' in arguments:
borgmatic.actions.browse.run.run_browse(
arguments['browse'],
arguments['global'],
configs,
)
except (
CalledProcessError,
ValueError,
+18 -4
View File
@@ -1091,6 +1091,15 @@ properties:
Corresponds to the "--stats" flag on those actions. Defaults to
false.
example: true
quick_statistics:
type: boolean
description: |
Display statistics for an archive when running supported actions,
skipping the repository-wide "All archives" and chunk index
statistics to save some time. Corresponds to the "--quick-stats"
flag on those actions. Defaults to false. (This option is supported
for Borg 1.4.5+ only.)
example: true
list_details:
type: boolean
description: |
@@ -1131,8 +1140,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 +1358,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 +1425,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
@@ -2825,7 +2837,9 @@ properties:
properties:
url:
type: string
description: URL of this Apprise service.
description: |
URL of this Apprise service. Supports the
"{credential ...}" syntax.
example: "gotify://hostname/token"
label:
type: string
+31 -7
View File
@@ -372,13 +372,21 @@ def log_buffer_lines(
yield log_record.getMessage()
def raise_for_process_errors(buffer_readers, process_metadatas, borg_local_path, borg_exit_codes):
def raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level,
borg_local_path,
borg_exit_codes,
capture_stderr=False,
):
'''
Given a dict from buffer object to Buffer_reader, a dict from subprocess.Popen() instance to
Process_metadata instance, Borg's local path, a sequence of exit code configuration dicts, check
the given processes for error or warning exit codes. If found, vent or kill any running
processes. In the case of an error exit code, raise. In the case of warning, return
Exit_status.WARNING. Otherwise, return None.
Process_metadata instance, a requested output log level for stdout, Borg's local path, a
sequence of exit code configuration dicts, and whether to capture stderr, check the given
processes for error or warning exit codes. If found, vent or kill any running processes and
drain any remaining buffer lines. In the case of an error exit code, raise. In the case of
warning, return Exit_status.WARNING. Otherwise, return None.
'''
result_status = None
@@ -404,6 +412,13 @@ def raise_for_process_errors(buffer_readers, process_metadatas, borg_local_path,
result_status = Exit_status.WARNING
continue
# Drain and log remaining buffer lines, so no output gets lost. But swallow captured output,
# since we're raising below instead of yielding captured lines.
tuple(
log_remaining_buffer_lines(
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr
)
)
last_lines = process_metadatas[process].last_lines
# If an error occurs, include its output in the raised exception so that we don't
@@ -429,6 +444,9 @@ def log_remaining_buffer_lines(
whether to capture stderr, drain and log any remaining output lines from the buffers until
they're empty. Additionally, for any log records with a log level the same as the output log
level, yield those log messages for capture.
The main difference between this function and log_buffer_lines() is that this one doesn't just
do one "turn of the crank" of logging buffer output; it completely drains any remaining output.
'''
for output_buffer, reader in buffer_readers.items():
if not reader.process:
@@ -446,6 +464,7 @@ def log_remaining_buffer_lines(
borg_local_path=borg_local_path,
command=reader.process.args,
),
last_lines=process_metadatas[reader.process].last_lines,
)
if (
@@ -494,7 +513,7 @@ def log_outputs(
for buffer in output_buffers_for_process(process, exclude_stdouts)
}
# Log output lines for each process until they all exit.
# Log output lines for each process until they all exit or one errors.
while True:
yield from log_buffer_lines(
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr
@@ -502,7 +521,12 @@ def log_outputs(
if (
raise_for_process_errors(
buffer_readers, process_metadatas, borg_local_path, borg_exit_codes
buffer_readers,
process_metadatas,
output_log_level,
borg_local_path,
borg_exit_codes,
capture_stderr,
)
== Exit_status.WARNING
):
+10 -2
View File
@@ -1,6 +1,7 @@
import logging
import operator
import borgmatic.hooks.credential.parse
import borgmatic.hooks.monitoring.logs
import borgmatic.hooks.monitoring.monitor
@@ -44,7 +45,9 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
import apprise # noqa: PLC0415
from apprise import NotifyFormat, NotifyType # noqa: PLC0415
except ImportError: # pragma: no cover
logger.warning('Unable to import Apprise in monitoring hook')
logger.warning(
'Unable to import Apprise in its monitoring hook; try installing "borgmatic[Apprise]"'
)
return
state_to_notify_type = {
@@ -76,7 +79,12 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
logger.info(f'Pinging Apprise services: {labels_string}{dry_run_string}')
apprise_object = apprise.Apprise()
apprise_object.add(list(map(operator.itemgetter('url'), hook_config.get('services'))))
apprise_object.add(
[
borgmatic.hooks.credential.parse.resolve_credential(service['url'], config)
for service in hook_config.get('services')
]
)
if dry_run:
return
+1 -1
View File
@@ -169,7 +169,7 @@ class Json_formatter(logging.Formatter):
def __init__(self, fmt='{message}', *args, style='{', **kwargs):
super().__init__(*args, fmt=fmt, style=style, **kwargs)
def format(self, record): # noqa: PLR6301
def format(self, record):
return log_record_to_json(record)
+1 -1
View File
@@ -5,7 +5,7 @@ RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
RUN pip install --break-system-packages --no-cache /app && borgmatic config generate && borgmatic config generate --destination /etc/borgmatic --split && chmod +r /etc/borgmatic/*.yaml
RUN mkdir /command-line \
&& borgmatic --help > /command-line/global.txt \
&& for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" "config show" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key import" "key change-passphrase" recreate diff borg; do \
&& for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" "config show" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key import" "key change-passphrase" recreate diff browse borg; do \
borgmatic $action --help > /command-line/${action/ /-}.txt; done
RUN /app/docs/fetch-contributors >> /contributors.html
@@ -1,5 +1,5 @@
Here's the command-line help for this action in the [most recent version of
borgmatic](https://projects.torsion.org/borgmatic-collective/borgmatic/releases).
If you're using an older version, some of these flags may not work, and you
should instead run the action with `--help` to see the flags specific to your
borgmatic version.
If you're using an older version, some of these flags may not work (or the action
may be missing entirely). You should instead run the action with `--help` to see
the flags and actions specific to your borgmatic version.
+9 -2
View File
@@ -31,7 +31,7 @@ changes work:
```bash
cd borgmatic
uv tool update-shell
uv tool install --editable .
uv tool install --editable .[dev]
```
Or to work on the [Apprise
@@ -39,7 +39,14 @@ hook](https://torsion.org/borgmatic/reference/configuration/monitoring/apprise/)
change that last line to:
```bash
uv tool install --editable .[Apprise]
uv tool install --editable .[dev,Apprise]
```
Or to work on the [browse
action](https://torsion.org/borgmatic/reference/command-line/actions/browse/):
```bash
uv tool install --editable .[dev,browse]
```
To get oriented with the borgmatic source code, have a look at the [source
+49
View File
@@ -135,3 +135,52 @@ is, patterns are _not_ taken into consideration. If you require this, supply the
See the [Borg](https://borgbackup.readthedocs.io/en/stable/usage/diff.html)
documentation for information on output format, what is compared, and more.
## Browsing backups
<span class="minilink minilink-addedin">New in version 2.1.6</span> <span
class="minilink minilink-addedin">Experimental feature</span> borgmatic has an
experimental console UI for browsing your repositories, archives, and files.
Here's what it looks like:
<img src="https://torsion.org/borgmatic/static/browse.png" alt="borgmatic browse screenshot" style="width: 100%">
This feature is not intended to be a general-purpose Borg UI with every
borgmatic feature, but rather it's for use cases like quickly looking at the
contents of your backups when you're feeling too lazy to type out a full
borgmatic command-line.
Depending on how you installed borgmatic, it may not have come with the
necessary Python libraries to support the browse action. (borgmatic's
[stand-alone
binary](https://projects.torsion.org/borgmatic-collective/borgmatic/releases)
does not currently include them.) If you originally [installed borgmatic with
uv](https://torsion.org/borgmatic/how-to/install-borgmatic/), run the following
to install the libraries needed for the browse action:
```bash
sudo uv tool install borgmatic[browse]
```
Omit `sudo` if borgmatic is installed as a non-root user.
Once the libraries are installed, run the following to access the browse action:
```bash
borgmatic browse
```
This launches a console UI where you can select a borgmatic configuration file
(if there's more than one), select a Borg repository, select an archive in that
repository, and even browse the backed up files in that archive.
Use the keyboard or the mouse to navigate the UI. The footer at the bottom of
the screen shows some of the available keys. Logs show up directly in the UI at
the [selected
verbosity](https://torsion.org/borgmatic/reference/command-line/logging/),
although logs are hidden by default.
Please [provide
feedback](https://torsion.org/borgmatic/#support-and-contributing) if you find
this feature useful—or even if you don't, but would like it to become useful.
+1 -1
View File
@@ -98,7 +98,7 @@ installing borgmatic:
#### Etc.
* [stand-alone Linux binary](https://projects.torsion.org/borgmatic-collective/borgmatic/releases) (This is a beta feature!)
* [stand-alone Linux binary](https://projects.torsion.org/borgmatic-collective/borgmatic/releases) (This is a beta feature! The minimal binary omits [Apprise support](https://torsion.org/borgmatic/reference/configuration/monitoring/apprise/).)
* [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
* [pipx](https://pipx.pypa.io/stable/)
@@ -0,0 +1,17 @@
---
title: browse
eleventyNavigation:
key: browse
parent: 🎬 Actions
---
<span class="minilink minilink-addedin">Experimental feature</span> {% include snippet/command-line/sample.md %}
```
{% include borgmatic/command-line/browse.txt %}
```
## Related documentation
* [Inspect your backups](https://torsion.org/borgmatic/how-to/inspect-your-backups/)
@@ -24,9 +24,14 @@ databases](https://torsion.org/borgmatic/how-to/backup-your-databases/#all-datab
borgmatic excludes most data coming from [MariaDB system
databases](https://mariadb.com/docs/server/reference/system-tables),
because much of it is populated on MariaDB startup and thus not restorable (or
just unnecessary to backup). The system data that borgmatic does include in
these dumps are: users, roles, grants, user-defined functions, and remote
servers.
just unnecessary to backup).
The system data that borgmatic does include in these dumps are: users, roles,
grants, user-defined functions, and remote servers—all from the `mysql` system
database. This omits all other data from the `mysql` database, which includes
index and table statistics, time zones, and installed server plugins. It also
excludes the separate `information_schema`, `performance_schema`, and `sys`
system databases. This is not currently configurable.
Within a Borg archive, you can find this data stored in a dump named `mysql`—the
name of the system table this data comes from. And if you'd like to dump this
@@ -5,9 +5,9 @@ eleventyNavigation:
parent: 🚨 Monitoring
---
<span class="minilink minilink-addedin">New in version 1.8.4</span>
[Apprise](https://github.com/caronc/apprise/wiki) is a local notification library
[Apprise](https://appriseit.com/) is a local notification library
that "allows you to send a notification to almost all of the most popular
[notification services](https://github.com/caronc/apprise/wiki) available to
[notification services](https://appriseit.com/services/) available to
us today such as: Telegram, Discord, Slack, Amazon SNS, Gotify, etc."
Depending on how you installed borgmatic, it may not have come with Apprise.
@@ -22,7 +22,7 @@ sudo uv tool install borgmatic[Apprise]
Omit `sudo` if borgmatic is installed as a non-root user.
Once Apprise is installed, configure borgmatic to notify one or more [Apprise
services](https://github.com/caronc/apprise/wiki). For example:
services](https://appriseit.com/services/). For example:
```yaml
apprise:
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+7 -4
View File
@@ -1,17 +1,18 @@
[project]
name = "borgmatic"
version = "2.1.6.dev0"
version = "2.1.6"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
description = "Simple, configuration-driven backup software for servers and workstations"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
readme = "README.md"
requires-python = ">=3.9"
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python",
"Topic :: Security :: Cryptography",
"Topic :: System :: Archiving :: Backup",
@@ -30,6 +31,8 @@ validate-borgmatic-config = "borgmatic.commands.validate_config:main"
[project.optional-dependencies]
Apprise = ["apprise"]
browse = ["textual", "binaryornot"]
browse-dev = ["textual-dev"]
[project.urls]
Homepage = "https://torsion.org/borgmatic"
@@ -44,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
@@ -54,7 +57,7 @@ exclude = ["*.*/*"]
quote-style = "preserve"
[tool.ruff.lint]
preview = true
preview = false
extend-select = [
"A", # flake8-builtins: builtin shadowing
"B", # flake8-bugbear: bugs and design problems
+12 -3
View File
@@ -43,15 +43,19 @@ twine upload -r pypi --username __token__ "$wheel_path"
scripts/export-docs-from-image
docs_path=dist/borgmatic-docs.tar.gz
# Build stand-alone binary.
# Build stand-alone binaries.
uv venv --python 3.13 --clear binary
source binary/bin/activate
uv pip install -r binary_requirements.txt nuitka[onefile]
nuitka --mode=onefile --enable-plugin=upx --include-package-data=borgmatic --include-data-dir=borgmatic.egg-info=borgmatic.egg-info --include-package=borgmatic.hooks --include-package=apprise --no-deployment-flag=self-execution borgmatic/commands/borgmatic.py
deactivate
rm -fr binary borgmatic.build borgmatic.dist
rm -fr borgmatic.build borgmatic.dist
standalone_binary_path="dist/borgmatic-${version}-binary-linux-glibc-x86_64"
mv borgmatic.bin "$standalone_binary_path"
nuitka --mode=onefile --enable-plugin=upx --include-package-data=borgmatic --include-data-dir=borgmatic.egg-info=borgmatic.egg-info --include-package=borgmatic.hooks --no-deployment-flag=self-execution borgmatic/commands/borgmatic.py
rm -fr binary borgmatic.build borgmatic.dist
deactivate
standalone_minimal_binary_path="dist/borgmatic-${version}-minimal-binary-linux-glibc-x86_64"
mv borgmatic.bin "$standalone_minimal_binary_path"
# Set release changelogs on projects.torsion.org and GitHub.
release_changelog="$(cat NEWS | sed '/^$/q' | grep -v '^\S')"
@@ -83,6 +87,11 @@ curl --silent --request POST \
--header "Authorization: token $projects_token" \
--header "Accept: application/json" \
--form attachment=@"$standalone_binary_path"
curl --silent --request POST \
"https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/releases/$release_id/assets?name=$(basename $standalone_minimal_binary_path)" \
--header "Authorization: token $projects_token" \
--header "Accept: application/json" \
--form attachment=@"$standalone_minimal_binary_path"
github-release create --token="$github_token" --owner=witten --repo=borgmatic --tag="$version" --target_commit="main" \
--name="borgmatic $version" --body="$release_changelog"
+4
View File
@@ -1,5 +1,6 @@
apprise
attrs
binaryornot
certifi
charset-normalizer
click>=8.1.8
@@ -16,13 +17,16 @@ packaging
pluggy
pygments
pytest
pytest-asyncio
pytest-cov
pytest-timeout
pyyaml>5.0.0
referencing
requests
requests-oauthlib
rich
rpds-py
ruamel-yaml>0.15.0
textual
typing-extensions
urllib3
+18 -8
View File
@@ -2,29 +2,39 @@
# uv pip compile --annotation-style line test_requirements.in -o test_requirements.txt
apprise==1.10.0 # via -r test_requirements.in
attrs==26.1.0 # via jsonschema, referencing, -r test_requirements.in
certifi==2026.4.22 # via apprise, requests, -r test_requirements.in
binaryornot==0.6.0 # via -r test_requirements.in
certifi==2026.5.20 # via apprise, requests, -r test_requirements.in
charset-normalizer==3.4.7 # via requests, -r test_requirements.in
click>=8.1.8
codespell==2.4.2 # via -r test_requirements.in
coverage==7.13.5 # via pytest-cov, -r test_requirements.in
coverage==7.14.0 # via pytest-cov, -r test_requirements.in
flexmock==0.13.0 # via -r test_requirements.in
idna==3.13 # via requests, -r test_requirements.in
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 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, -r test_requirements.in
pytest==9.0.3 # via pytest-cov, pytest-timeout, -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
pytest-timeout==2.4.0 # via -r test_requirements.in
pyyaml>5.0.0
referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r test_requirements.in
requests==2.33.1 # 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
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
urllib3==2.6.3 # via requests, -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
@@ -0,0 +1,68 @@
from flexmock import flexmock
import borgmatic.actions.browse.app
import borgmatic.actions.browse.configuration_files_list
import borgmatic.actions.browse.logs
import borgmatic.actions.browse.repositories_list
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'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
async with app.run_test():
app.query_one(selector='Header')
carousel = app.query_one(selector='Carousel')
assert len(carousel.panels) == 1
assert isinstance(
carousel.panels[0],
borgmatic.actions.browse.configuration_files_list.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'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
async with app.run_test():
app.query_one(selector='Header')
carousel = app.query_one(selector='Carousel')
assert len(carousel.panels) == 1
assert isinstance(
carousel.panels[0], borgmatic.actions.browse.repositories_list.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'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
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,86 @@
import textual.app
import textual.widgets.option_list
from flexmock import flexmock
from borgmatic.actions.browse import archives_list as module
async def test_archives_list_on_mount_does_not_raise():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(module.borgmatic.actions.browse.workers).should_receive('add_repository_archives')
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
flexmock(archives_list.archive_loaded).should_receive('subscribe')
async with textual.app.App().run_test():
archives_list.on_mount()
def test_archives_list_on_archive_loaded_with_loading_done_removes_loading_indicator():
loading_timer = flexmock()
flexmock(module.borgmatic.actions.browse.loading).should_receive(
'add_inline_loading_indicator'
).and_return(loading_timer)
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
flexmock(loading_timer).should_receive('stop')
flexmock(archives_list).should_receive('remove_option').with_args('loading-indicator').once()
flexmock(archives_list).should_receive('add_options').never()
archives_list.on_archive_loaded(module.borgmatic.actions.browse.workers.LOADING_DONE)
def test_archives_list_on_archive_loaded_adds_archive_name():
loading_timer = flexmock()
flexmock(module.borgmatic.actions.browse.loading).should_receive(
'add_inline_loading_indicator'
).and_return(loading_timer)
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
flexmock(loading_timer).should_receive('stop').never()
loading_indicator = flexmock()
flexmock(archives_list).should_receive('get_option').and_return(loading_indicator)
flexmock(archives_list).should_receive('remove_option').with_args('loading-indicator').once()
flexmock(archives_list).should_receive('add_options').once()
archives_list.on_archive_loaded('archive')
def test_archives_list_on_option_list_option_highlighted_with_highlighted_none_marks_it_unchanged():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
archives_list.highlighted = None
archives_list.on_option_list_option_highlighted(event=flexmock())
assert archives_list.highlighted_option_changed is False
def test_archives_list_on_option_list_option_highlighted_with_highlighted_zero_marks_it_unchanged():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
archives_list.highlighted = 0
archives_list.on_option_list_option_highlighted(event=flexmock())
assert archives_list.highlighted_option_changed is False
def test_archives_list_on_option_list_option_highlighted_with_existing_option_and_highlighted_zero_marks_it_unchanged():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
archives_list.add_option(textual.widgets.option_list.Option('zero', id='zero'))
archives_list.highlighted = 0
archives_list.on_option_list_option_highlighted(event=flexmock())
assert archives_list.highlighted_option_changed is False
def test_archives_list_on_option_list_option_highlighted_with_highlighted_non_zero_marks_it_changed():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
archives_list.add_option(textual.widgets.option_list.Option('zero', id='zero'))
archives_list.add_option(textual.widgets.option_list.Option('one', id='one'))
archives_list.highlighted = 1
archives_list.on_option_list_option_highlighted(event=flexmock())
assert archives_list.highlighted_option_changed is True
@@ -0,0 +1,522 @@
import pytest
import textual.widgets.option_list
from flexmock import flexmock
import borgmatic.actions.browse.app
import borgmatic.actions.browse.archive
import borgmatic.actions.browse.carousel
import borgmatic.actions.browse.loading
import borgmatic.actions.browse.logs
import borgmatic.actions.browse.workers
from borgmatic.actions.browse import carousel as module
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.configuration_files_list.Configuration_files_list(
configs
),
option_id='test.yaml',
)
assert isinstance(
repositories_list, borgmatic.actions.browse.repositories_list.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.archives_list.Archives_list).should_receive('app').and_return(
flexmock()
)
archives_list = module.make_next_panel(
focused_panel=borgmatic.actions.browse.repositories_list.Repositories_list(config),
option_id=0,
)
assert isinstance(archives_list, borgmatic.actions.browse.archives_list.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('load_archive_paths')
flexmock(borgmatic.actions.browse.archives_list.Archives_list).should_receive('app').and_return(
flexmock()
)
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
directory_list = module.make_next_panel(
focused_panel=borgmatic.actions.browse.archives_list.Archives_list(
config, config['repositories'][0]
),
option_id='archive',
)
assert isinstance(directory_list, borgmatic.actions.browse.directory_list.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('load_archive_paths')
flexmock(borgmatic.actions.browse.workers).should_receive('Archive_path_loaded').replace_with(
flexmock(
path_hierarchy={
'etc': {},
},
complete=False,
),
)
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
focused_panel = borgmatic.actions.browse.directory_list.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.directory_list.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('load_archive_paths')
flexmock(borgmatic.actions.browse.workers).should_receive('Archive_path_loaded').replace_with(
flexmock(
path_hierarchy={
'etc': {
'borgmatic': {},
},
},
complete=False,
),
)
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
focused_panel = borgmatic.actions.browse.directory_list.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.directory_list.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('load_archive_paths')
flexmock(borgmatic.actions.browse.workers).should_receive('load_file_preview')
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
flexmock(borgmatic.actions.browse.file_preview.File_preview).should_receive('app').and_return(
flexmock()
)
focused_panel = borgmatic.actions.browse.directory_list.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.file_preview.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('load_archive_paths')
flexmock(borgmatic.actions.browse.workers).should_receive('load_file_preview')
flexmock(borgmatic.actions.browse.workers).should_receive('Archive_path_loaded').replace_with(
flexmock(
path_hierarchy={
'etc': {
'borgmatic': {
'config.yaml': borgmatic.actions.browse.archive.Archive_path(
'-', 'config.yaml', ''
),
},
},
},
complete=False,
),
)
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
flexmock(borgmatic.actions.browse.file_preview.File_preview).should_receive('app').and_return(
flexmock()
)
focused_panel = borgmatic.actions.browse.directory_list.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.file_preview.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('load_archive_paths')
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
focused_panel = borgmatic.actions.browse.directory_list.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():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
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'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
async with app.run_test() as pilot:
await pilot.press('left')
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'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
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.configuration_files_list.Configuration_files_list,
)
assert carousel.panels[0].styles.display == 'none'
assert isinstance(
carousel.panels[1], borgmatic.actions.browse.repositories_list.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_panel():
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')
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.repositories_list.Repositories_list
)
assert carousel.panels[0].styles.display == 'none'
assert isinstance(carousel.panels[1], borgmatic.actions.browse.archives_list.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_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.repositories_list.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={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
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.configuration_files_list.Configuration_files_list,
)
assert carousel.panels[0].styles.display == 'block'
assert carousel.panels[0].highlighted == 0
assert isinstance(
carousel.panels[1], borgmatic.actions.browse.repositories_list.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.logs).should_receive('log_to_widget')
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.configuration_files_list.Configuration_files_list,
)
assert carousel.panels[0].styles.display == 'none'
assert isinstance(
carousel.panels[1], borgmatic.actions.browse.repositories_list.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_multiple_configs_and_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.logs).should_receive('log_to_widget')
flexmock(module).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.configuration_files_list.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'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
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.configuration_files_list.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'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
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.configuration_files_list.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'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
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.configuration_files_list.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'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
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.configuration_files_list.Configuration_files_list,
)
assert carousel.panels[0].styles.display == 'block'
assert carousel.panels[0].highlighted == 0
assert isinstance(
carousel.panels[1], borgmatic.actions.browse.repositories_list.Repositories_list
)
assert carousel.panels[1].styles.display == 'none'
assert app.focused == carousel.panels[0]
@@ -0,0 +1,34 @@
from flexmock import flexmock
from borgmatic.actions.browse import configuration_files_list as module
def test_configuration_files_list_adds_config_paths_as_options():
flexmock(module.os.path).should_receive('expanduser').and_return('/home/user')
configuration_files_list = module.Configuration_files_list(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
assert len(configuration_files_list.options) == 2
assert configuration_files_list.options[0].prompt == 'test1.yaml'
assert configuration_files_list.options[0].id == 'test1.yaml'
assert configuration_files_list.options[1].prompt == 'test2.yaml'
assert configuration_files_list.options[1].id == 'test2.yaml'
def test_configuration_files_list_collapses_home_directory_in_config_path_option():
flexmock(module.os.path).should_receive('expanduser').and_return('/home/user')
configuration_files_list = module.Configuration_files_list(
configs={
'/home/user/test.yaml': {'repositories': [{'path': '/home/user/test.borg'}]},
}
)
assert len(configuration_files_list.options) == 1
assert configuration_files_list.options[0].prompt == '~/test.yaml'
assert configuration_files_list.options[0].id == '/home/user/test.yaml'
@@ -0,0 +1,287 @@
import textual.widgets
import textual.widgets.option_list
from flexmock import flexmock
from borgmatic.actions.browse import directory_list as module
def test_add_archive_paths_with_only_duplicate_paths_bails():
directory_list = textual.widgets.OptionList()
directory_list.path_components = ('etc',)
directory_list.add_option(textual.widgets.option_list.Option('foo', id='foo'))
directory_list.add_option(textual.widgets.option_list.Option('bar', id='bar'))
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(directory_list).should_receive('set_options').never()
module.add_archive_paths(
directory_list=directory_list,
config=config,
repository=config['repositories'][0],
archive_name='archive',
archive_paths=(
flexmock(path_type='-', file_path='etc/foo/one.txt', link_target=''),
flexmock(path_type='-', file_path='etc/foo/two.txt', link_target=''),
),
)
def test_add_archive_paths_adds_ands_sorts_and_filters_and_deduplicates():
directory_list = textual.widgets.OptionList()
directory_list.path_components = ('etc',)
directory_list.add_option(textual.widgets.option_list.Option('📄 foo', id='foo'))
directory_list.add_option(textual.widgets.option_list.Option('📄 bar', id='bar'))
directory_list.highlighted = 1
directory_list.highlighted_option_changed = True
config = {'repositories': [{'path': 'test.borg'}]}
module.add_archive_paths(
directory_list=directory_list,
config=config,
repository=config['repositories'][0],
archive_name='archive',
archive_paths=(
flexmock(path_type='d', file_path='etc/quux', link_target=''),
flexmock(path_type='-', file_path='etc/foo', link_target=''),
flexmock(path_type='-', file_path='etc/baz', link_target=''),
flexmock(path_type='d', file_path='root/nope', link_target=''),
flexmock(path_type='d', file_path='etc/other', link_target=''),
),
)
assert len(directory_list.options) == 5
assert directory_list.options[0].prompt == '📁 other'
assert directory_list.options[0].id == 'other'
assert directory_list.options[1].prompt == '📁 quux'
assert directory_list.options[1].id == 'quux'
assert directory_list.options[2].prompt == '📄 bar'
assert directory_list.options[2].id == 'bar'
assert directory_list.options[3].prompt == '📄 baz'
assert directory_list.options[3].id == 'baz'
assert directory_list.options[4].prompt == '📄 foo'
assert directory_list.options[4].id == 'foo'
assert directory_list.highlighted == 2
def test_add_archive_paths_highlights_first_option_if_highlight_has_not_changed():
directory_list = textual.widgets.OptionList()
directory_list.path_components = ('etc',)
directory_list.add_option(textual.widgets.option_list.Option('📄 foo', id='foo'))
directory_list.add_option(textual.widgets.option_list.Option('📄 bar', id='bar'))
directory_list.highlighted = None
directory_list.highlighted_option_changed = False
config = {'repositories': [{'path': 'test.borg'}]}
module.add_archive_paths(
directory_list=directory_list,
config=config,
repository=config['repositories'][0],
archive_name='archive',
archive_paths=(flexmock(path_type='-', file_path='etc/baz', link_target=''),),
)
assert len(directory_list.options) == 3
assert directory_list.options[0].prompt == '📄 bar'
assert directory_list.options[0].id == 'bar'
assert directory_list.options[1].prompt == '📄 baz'
assert directory_list.options[1].id == 'baz'
assert directory_list.options[2].prompt == '📄 foo'
assert directory_list.options[2].id == 'foo'
assert directory_list.highlighted == 0
def test_add_archive_paths_retains_loading_indicator_at_bottom():
directory_list = textual.widgets.OptionList()
directory_list.path_components = ('etc',)
directory_list.add_option(textual.widgets.option_list.Option('📄 foo', id='foo'))
directory_list.add_option(textual.widgets.option_list.Option('📄 bar', id='bar'))
directory_list.add_option(
textual.widgets.option_list.Option('loading!!!', id='loading-indicator')
)
directory_list.highlighted = 0
directory_list.highlighted_option_changed = True
config = {'repositories': [{'path': 'test.borg'}]}
module.add_archive_paths(
directory_list=directory_list,
config=config,
repository=config['repositories'][0],
archive_name='archive',
archive_paths=(flexmock(path_type='-', file_path='etc/baz', link_target=''),),
)
assert len(directory_list.options) == 4
assert directory_list.options[0].prompt == '📄 bar'
assert directory_list.options[0].id == 'bar'
assert directory_list.options[1].prompt == '📄 baz'
assert directory_list.options[1].id == 'baz'
assert directory_list.options[2].prompt == '📄 foo'
assert directory_list.options[2].id == 'foo'
assert directory_list.options[3].prompt == 'loading!!!'
assert directory_list.options[3].id == 'loading-indicator'
assert directory_list.highlighted == 2
def test_directory_list_with_root_directory_adds_loading_indicator():
flexmock(module.borgmatic.actions.browse.loading).should_receive(
'add_inline_loading_indicator'
).once()
directory_list = module.Directory_list(
config=flexmock(), repository=flexmock(), archive_name='archive'
)
assert directory_list.border_title == '📁 archive'
assert len(directory_list.options) == 0
assert not directory_list.path_loaded.complete
def test_directory_list_with_already_complete_loading_skips_loading_indicator():
flexmock(module.borgmatic.actions.browse.loading).should_receive(
'add_inline_loading_indicator'
).never()
flexmock(module.borgmatic.actions.browse.workers).should_receive('load_archive_paths').never()
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
directory_list = module.Directory_list(
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=True),
path_components=('etc',),
)
assert directory_list.border_title == '📁 etc'
assert len(directory_list.options) == 1
assert directory_list.options[0].prompt == '📁 ..'
assert directory_list.options[0].id == '..'
def test_directory_list_on_mount_with_root_directory_loads_archive_paths():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
flexmock(module.borgmatic.actions.browse.workers).should_receive('load_archive_paths').once()
flexmock(module).should_receive('add_archive_paths').never()
directory_list = module.Directory_list(
config=flexmock(), repository=flexmock(), archive_name='archive'
)
flexmock(directory_list.path_loaded).should_receive('subscribe')
directory_list.on_mount()
def test_directory_list_on_mount_with_non_root_directory_adds_archive_paths():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
flexmock(module.borgmatic.actions.browse.workers).should_receive('load_archive_paths').never()
flexmock(module).should_receive('add_archive_paths').once()
directory_list = module.Directory_list(
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
flexmock(directory_list.path_loaded).should_receive('subscribe')
directory_list.on_mount()
def test_on_archive_path_loaded_with_loading_done_signal_removes_loading_indicator():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
flexmock(module).should_receive('add_archive_paths').never()
directory_list = module.Directory_list(
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
directory_list.timer = flexmock(stop=lambda: None)
flexmock(directory_list).should_receive('remove_option').once()
directory_list.on_archive_path_loaded(data=module.borgmatic.actions.browse.workers.LOADING_DONE)
def test_on_archive_path_loaded_with_path_loaded_signal_adds_archive_path():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
flexmock(module).should_receive('add_archive_paths').once()
directory_list = module.Directory_list(
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
flexmock(directory_list).should_receive('remove_option').never()
directory_list.on_archive_path_loaded(data=flexmock())
def test_directory_list_on_option_list_option_highlighted_with_highlighted_none_marks_it_unchanged():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
directory_list = module.Directory_list(
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
directory_list.highlighted = None
directory_list.on_option_list_option_highlighted(event=flexmock())
assert directory_list.highlighted_option_changed is False
def test_directory_list_on_option_list_option_highlighted_with_highlighted_zero_marks_it_unchanged():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
directory_list = module.Directory_list(
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
directory_list.add_option(textual.widgets.option_list.Option('zero', id='zero'))
directory_list.highlighted = 0
directory_list.on_option_list_option_highlighted(event=flexmock())
assert directory_list.highlighted_option_changed is False
def test_directory_list_on_option_list_option_highlighted_with_highlighted_non_zero_marks_it_changed():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
'app'
).and_return(flexmock())
directory_list = module.Directory_list(
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
directory_list.add_option(textual.widgets.option_list.Option('zero', id='zero'))
directory_list.add_option(textual.widgets.option_list.Option('one', id='one'))
directory_list.highlighted = 1
directory_list.on_option_list_option_highlighted(event=flexmock())
assert directory_list.highlighted_option_changed is True
@@ -0,0 +1,53 @@
import textual.widgets.option_list
from flexmock import flexmock
from borgmatic.actions.browse import file_preview as module
def test_file_preview_does_not_raise():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
module.File_preview(
config=flexmock(), repository=flexmock(), archive_name='archive', file_path='foo/bar.txt'
)
async def test_file_preview_on_mount_does_not_raise():
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(module.borgmatic.actions.browse.workers).should_receive('load_file_preview')
file_preview = module.File_preview(
config=flexmock(), repository=flexmock(), archive_name='archive', file_path='foo.txt'
)
flexmock(file_preview.file_preview_loaded).should_receive('subscribe')
async with textual.app.App().run_test():
file_preview.on_mount()
def test_file_preview_on_file_preview_loaded_with_none_file_contents_displays_error():
flexmock(module.borgmatic.actions.browse.loading).should_receive(
'add_inline_loading_indicator'
).and_return(flexmock(stop=lambda: None))
file_preview = module.File_preview(
config=flexmock(), repository=flexmock(), archive_name='archive', file_path='foo.txt'
)
flexmock(file_preview).should_receive('write').with_args(
'Cannot display a preview for this file'
).once()
file_preview.on_file_preview_loaded(None)
def test_file_preview_on_file_preview_loaded_with_file_contents_displays_contents():
flexmock(module.borgmatic.actions.browse.loading).should_receive(
'add_inline_loading_indicator'
).and_return(flexmock(stop=lambda: None))
file_preview = module.File_preview(
config=flexmock(), repository=flexmock(), archive_name='archive', file_path='foo.txt'
)
flexmock(file_preview).should_receive('write').with_args(
'Cannot display a preview for this file'
).never()
flexmock(file_preview).should_receive('write').once()
file_preview.on_file_preview_loaded('hi')
@@ -0,0 +1,97 @@
import pytest
import textual.app
import textual.widgets
from flexmock import flexmock
from borgmatic.actions.browse import loading as module
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'))
module.update_inline_loading_indicator(widget)
assert len(widget.options) == 1
assert widget.options[0].prompt == 'HOLD..'
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'))
module.update_inline_loading_indicator(widget)
assert len(widget.options) == 1
assert widget.options[0].prompt == 'HOLD'
def test_update_inline_loading_indicator_with_option_list_and_missing_indicator_does_not_raise():
widget = textual.widgets.OptionList()
module.update_inline_loading_indicator(widget)
assert len(widget.options) == 0
async def test_update_inline_loading_indicator_with_rich_log_adds_a_dot():
async with textual.app.App().run_test():
widget = textual.widgets.RichLog()
widget._size_known = True
widget.write('HOLD.')
module.update_inline_loading_indicator(widget)
assert str(widget.lines[0].text) == 'HOLD..'
async def test_update_inline_loading_indicator_with_rich_log_wraps_dots_beyond_three():
async with textual.app.App().run_test():
widget = textual.widgets.RichLog()
widget._size_known = True
widget.write('HOLD...')
module.update_inline_loading_indicator(widget)
assert str(widget.lines[0].text) == 'HOLD'
def test_update_inline_loading_indicator_with_rich_log_and_missing_indicator_does_not_raise():
widget = textual.widgets.RichLog()
module.update_inline_loading_indicator(widget)
assert len(widget.lines) == 0
def test_update_inline_loading_indicator_with_unsupported_widget_type_raises():
with pytest.raises(ValueError):
module.update_inline_loading_indicator(flexmock())
def test_add_inline_loading_indicator_with_option_list_adds_loading_indicator_option():
widget = textual.widgets.OptionList()
flexmock(widget).should_receive('set_interval')
module.add_inline_loading_indicator(widget)
assert len(widget.options) == 1
assert widget.options[0].prompt == module.LOADING_MESSAGE
assert widget.options[0].id == 'loading-indicator'
assert widget.highlighted is None
async def test_add_inline_loading_indicator_with_rich_log_writes_loading_indicator_text():
async with textual.app.App().run_test():
widget = textual.widgets.RichLog()
widget._size_known = True
flexmock(widget).should_receive('set_interval')
module.add_inline_loading_indicator(widget)
assert str(widget.lines[0].text) == module.LOADING_MESSAGE
def test_add_inline_loading_indicator_with_unsupported_widget_type_raises():
with pytest.raises(ValueError):
module.add_inline_loading_indicator(flexmock())
@@ -0,0 +1,37 @@
import contextlib
from flexmock import flexmock
from borgmatic.actions.browse import logs as module
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()
@@ -0,0 +1,12 @@
from borgmatic.actions.browse import repositories_list as module
def test_repositories_list_populates_options():
config = {'repositories': [{'path': 'test1.borg'}, {'path': 'test2.borg', 'label': 'two'}]}
repositories_list = module.Repositories_list(config=config)
assert len(repositories_list.options) == 2
assert repositories_list.options[0].prompt == 'test1.borg'
assert repositories_list.options[0].id == 0
assert repositories_list.options[1].prompt == 'two'
assert repositories_list.options[1].id == 1
@@ -0,0 +1,19 @@
from flexmock import flexmock
from borgmatic.actions.browse import workers as module
def test_archive_path_loaded_publish_records_complete():
signal = module.Archive_path_loaded(owner=flexmock(), name='Bob')
signal.publish(module.LOADING_DONE)
assert signal.complete
def test_archive_path_loaded_publish_records_published_path():
archive_path = module.borgmatic.actions.browse.archive.Archive_path('-', 'foo/bar.txt', '')
signal = module.Archive_path_loaded(owner=flexmock(), name='Bob')
signal.publish(archive_path)
assert signal.path_hierarchy == {'foo': {'bar.txt': archive_path}}
assert not signal.complete
+12
View File
@@ -57,6 +57,8 @@ def test_make_rename_command_includes_log_debug():
def test_make_rename_command_includes_dry_run():
insert_logging_mock(logging.WARNING)
command = module.make_rename_command(
dry_run=True,
repository_name='repo',
@@ -79,6 +81,8 @@ def test_make_rename_command_includes_dry_run():
def test_make_rename_command_includes_remote_path():
insert_logging_mock(logging.WARNING)
command = module.make_rename_command(
dry_run=False,
repository_name='repo',
@@ -102,6 +106,8 @@ def test_make_rename_command_includes_remote_path():
def test_make_rename_command_includes_umask():
insert_logging_mock(logging.WARNING)
command = module.make_rename_command(
dry_run=False,
repository_name='repo',
@@ -125,6 +131,8 @@ def test_make_rename_command_includes_umask():
def test_make_rename_command_includes_log_json():
insert_logging_mock(logging.WARNING)
command = module.make_rename_command(
dry_run=False,
repository_name='repo',
@@ -140,6 +148,8 @@ def test_make_rename_command_includes_log_json():
def test_make_rename_command_includes_lock_wait():
insert_logging_mock(logging.WARNING)
command = module.make_rename_command(
dry_run=False,
repository_name='repo',
@@ -163,6 +173,8 @@ def test_make_rename_command_includes_lock_wait():
def test_make_rename_command_includes_extra_borg_options():
insert_logging_mock(logging.WARNING)
command = module.make_rename_command(
dry_run=False,
repository_name='repo',
+37
View File
@@ -557,6 +557,43 @@ def test_log_outputs_with_unfinished_process_re_polls():
)
def test_log_outputs_includes_error_output_when_output_spans_multiple_chunks():
flexmock(module.logger).should_receive('log')
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('command_for_process').and_return('python')
process = subprocess.Popen(
[
sys.executable,
'-c',
(
'import os, sys; '
f'os.write(sys.stdout.fileno(), b"x" * {module.READ_CHUNK_SIZE + 10}); '
'os.write(sys.stdout.fileno(), b"\\nERROR: critical failure"); '
'os.close(1); '
'os._exit(2)'
),
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
flexmock(module).should_receive('output_buffers_for_process').and_return((process.stdout,))
with pytest.raises(subprocess.CalledProcessError) as error:
tuple(
module.log_outputs(
(process,),
exclude_stdouts=(),
output_log_level=logging.INFO,
borg_local_path='borg',
borg_exit_codes=None,
)
)
assert error.value.output
assert 'ERROR: critical failure' in error.value.output
def test_read_lines_uses_system_locale_when_decoding_output():
flexmock(module.locale).should_receive('getpreferredencoding').and_return('ISO-8859-1')
process = subprocess.Popen(['echo', b'\xc4pple'], stdout=subprocess.PIPE)
+90
View File
@@ -0,0 +1,90 @@
from flexmock import flexmock
from borgmatic.actions.browse import archive as module
def test_get_repository_archives_does_not_raise():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
flexmock(module.borgmatic.borg.repo_list).should_receive('list_repository').and_return('{}')
assert module.get_repository_archives(config, config['repositories'][0]) == {}
def test_get_archive_paths_returns_each_as_archive_path_with_metadata():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
{'path': 'foo.txt', 'type': '-', 'linktarget': ''},
{'path': 'bar.txt', 'type': 'l', 'linktarget': 'foo.txt'},
{'path': 'etc', 'type': 'd', 'linktarget': ''},
)
assert tuple(module.get_archive_paths(config, config['repositories'][0], 'archive')) == (
module.Archive_path('-', 'foo.txt', ''),
module.Archive_path('l', 'bar.txt', 'foo.txt'),
module.Archive_path('d', 'etc', ''),
)
def test_get_archive_file_content_with_binary_file_bails():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
flexmock(stdout=flexmock(readlines=lambda hint: [b'foo\n', b'bar\n'])),
)
flexmock(module.binaryornot.helpers).should_receive('is_binary_string').and_return(True)
assert (
module.get_archive_file_content(config, config['repositories'][0], 'archive', 'etc/foo.txt')
is None
)
def test_get_archive_file_content_decodes_content():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
flexmock(stdout=flexmock(readlines=lambda hint: [b'foo\n', b'bar\n'])),
)
flexmock(module.binaryornot.helpers).should_receive('is_binary_string').and_return(False)
assert (
module.get_archive_file_content(config, config['repositories'][0], 'archive', 'etc/foo.txt')
== 'foo\nbar\n'
)
def test_get_archive_file_content_with_large_file_truncates_content():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
flexmock(module).READLINES_HINT_BYTES = 3
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
flexmock(stdout=flexmock(readlines=lambda hint: [b'foo\n', b'bar\n'])),
)
flexmock(module.binaryornot.helpers).should_receive('is_binary_string').and_return(False)
assert (
module.get_archive_file_content(config, config['repositories'][0], 'archive', 'etc/foo.txt')
== f'foo\nbar\n\n{module.TRUNCATION_MESSAGE}'
)
def test_get_archive_file_content_with_unicode_decode_error_does_not_raise():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
flexmock(stdout=flexmock(readlines=lambda hint: [b'foo\n', b'\xc3\n'])),
)
flexmock(module.binaryornot.helpers).should_receive('is_binary_string').and_return(False)
assert (
module.get_archive_file_content(config, config['repositories'][0], 'archive', 'etc/foo.txt')
is None
)
@@ -0,0 +1,94 @@
from flexmock import flexmock
from borgmatic.actions.browse import directory_list as module
def test_get_relative_archive_path_components_strips_off_current_directory():
assert module.get_relative_archive_path_components(
flexmock(file_path='foo/bar/baz/quux.txt'), ('foo', 'bar')
) == ('baz', 'quux.txt')
def test_get_relative_archive_path_components_with_root_current_directory_strips_off_nothing():
assert module.get_relative_archive_path_components(
flexmock(file_path='foo/bar/baz/quux.txt'), ()
) == ('foo', 'bar', 'baz', 'quux.txt')
def test_get_relative_archive_path_components_with_non_matching_paths_returns_none():
assert (
module.get_relative_archive_path_components(
flexmock(file_path='foo/bar/baz/quux.txt'), ('etc',)
)
is None
)
def test_make_directory_list_option_with_file_path_makes_file_option():
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
option = module.make_directory_list_option(
flexmock(path_type='-', file_path='foo/bar/baz.txt', link_target=''), ('baz.txt',)
)
assert option.prompt == '📄 baz.txt'
assert option.id == 'baz.txt'
def test_make_directory_list_option_with_directory_path_makes_directory_option():
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
option = module.make_directory_list_option(
flexmock(path_type='d', file_path='foo/bar/baz', link_target=''), ('baz',)
)
assert option.prompt == '📁 baz'
assert option.id == 'baz'
def test_make_directory_list_option_with_contained_file_path_makes_directory_option():
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
option = module.make_directory_list_option(
flexmock(path_type='d', file_path='foo/bar/baz.txt', link_target=''),
(
'bar',
'baz.txt',
),
)
assert option.prompt == '📁 bar'
assert option.id == 'bar'
def test_make_directory_list_option_with_link_path_makes_link_option():
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
option = module.make_directory_list_option(
flexmock(path_type='l', file_path='foo/bar/baz.txt', link_target='quux.txt'), ('baz.txt',)
)
assert option.prompt == '🔗 baz.txt → quux.txt'
assert option.id == 'baz.txt'
def test_make_directory_list_option_with_pipe_path_makes_pipe_option():
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
option = module.make_directory_list_option(
flexmock(path_type='p', file_path='foo/bar/baz.txt', link_target=''), ('baz.txt',)
)
assert option.prompt == '🚰 baz.txt'
assert option.id == 'baz.txt'
def test_make_directory_list_option_with_unknown_path_makes_unknown_option():
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
option = module.make_directory_list_option(
flexmock(path_type='wtf', file_path='foo/bar/baz.txt', link_target=''), ('baz.txt',)
)
assert option.prompt == '❓ baz.txt'
assert option.id == 'baz.txt'
+87
View File
@@ -0,0 +1,87 @@
from flexmock import flexmock
from borgmatic.actions.browse import logs as module
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',
),
),
)
+29
View File
@@ -0,0 +1,29 @@
from flexmock import flexmock
import borgmatic.actions.browse.app
from borgmatic.actions.browse import run as module
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()),
)
+126
View File
@@ -0,0 +1,126 @@
import pytest
from flexmock import flexmock
from borgmatic.actions.browse import workers as module
def test_add_repository_archives_publishes_for_each_archive():
flexmock(module.borgmatic.actions.browse.archive).should_receive(
'get_repository_archives'
).and_return({'archives': [{'archive': 'foo'}, {'archive': 'bar'}]})
archive_loaded = flexmock()
archive_loaded.should_receive('publish').with_args('bar').once() # Reversed order.
archive_loaded.should_receive('publish').with_args('foo').once()
archive_loaded.should_receive('publish').with_args(module.LOADING_DONE).once()
module.add_repository_archives.__wrapped__(
browse_app=flexmock(),
archive_loaded=archive_loaded,
config=flexmock(),
repository=flexmock(),
)
def test_record_path_sets_file_path_in_filesystem_hierarchy():
archive_path = flexmock(path_type='-', file_path='foo/bar/baz.txt')
quux_path = flexmock()
hierarchy = {'foo': {'bar': {'quux.txt': quux_path}, 'empty': {}}}
module.record_path(
archive_path=archive_path, hierarchy=hierarchy, path_components=('foo', 'bar', 'baz.txt')
)
assert hierarchy == {
'foo': {'bar': {'quux.txt': quux_path, 'baz.txt': archive_path}, 'empty': {}}
}
def test_record_path_sets_directory_path_in_filesystem_hierarchy():
archive_path = flexmock(path_type='d', file_path='foo/bar/baz')
quux_path = flexmock()
hierarchy = {'foo': {'bar': {'quux.txt': quux_path}, 'empty': {}}}
module.record_path(
archive_path=archive_path, hierarchy=hierarchy, path_components=('foo', 'bar', 'baz')
)
assert hierarchy == {'foo': {'bar': {'quux.txt': quux_path, 'baz': {}}, 'empty': {}}}
def test_get_paths_lists_directory_contents():
baz_path = module.borgmatic.actions.browse.archive.Archive_path('-', 'foo/bar/baz.txt', '')
hierarchy = {'foo': {'bar': {'baz.txt': baz_path, 'other': {}}}, 'nope': {}}
assert tuple(module.get_paths(hierarchy, ('foo', 'bar'))) == (
baz_path,
module.borgmatic.actions.browse.archive.Archive_path('d', 'foo/bar/other', ''),
)
def test_get_paths_with_full_path_components_lists_directory_contents():
baz_path = module.borgmatic.actions.browse.archive.Archive_path('-', 'etc/foo/bar/baz.txt', '')
hierarchy = {'foo': {'bar': {'baz.txt': baz_path, 'other': {}}}, 'nope': {}}
assert tuple(module.get_paths(hierarchy, ('foo', 'bar'), ('etc', 'foo', 'bar'))) == (
baz_path,
module.borgmatic.actions.browse.archive.Archive_path('d', 'etc/foo/bar/other', ''),
)
def test_get_paths_lists_empty_directory_contents():
baz_path = module.borgmatic.actions.browse.archive.Archive_path('-', 'etc/foo/bar/baz.txt', '')
hierarchy = {'foo': {'bar': {'baz.txt': baz_path, 'other': {}}}, 'nope': {}}
assert (
tuple(module.get_paths(hierarchy, ('foo', 'bar', 'other'), ('etc', 'foo', 'bar', 'other')))
== ()
)
def test_get_paths_with_unknown_file_raises():
hierarchy = {'foo': {'other': {}}}
with pytest.raises(ValueError):
tuple(module.get_paths(hierarchy, ('foo', 'bar.txt')))
def test_get_paths_with_unknown_directory_raises():
hierarchy = {'foo': {'other': {}}}
with pytest.raises(ValueError):
tuple(module.get_paths(hierarchy, ('foo', 'bar', 'baz')))
def test_load_archive_paths_publishes_for_each_archive_path():
flexmock(module.borgmatic.actions.browse.archive).should_receive('get_archive_paths').and_yield(
'foo.txt', 'bar.txt'
)
path_loaded = flexmock()
path_loaded.should_receive('publish').with_args('foo.txt').once()
path_loaded.should_receive('publish').with_args('bar.txt').once()
path_loaded.should_receive('publish').with_args(module.LOADING_DONE).once()
module.load_archive_paths.__wrapped__(
browse_app=flexmock(),
path_loaded=path_loaded,
config=flexmock(),
repository=flexmock(),
archive_name='archive',
)
def test_load_file_preview_publishes_file_contents():
flexmock(module.borgmatic.actions.browse.archive).should_receive(
'get_archive_file_content'
).and_return('hi')
file_preview_loaded = flexmock()
file_preview_loaded.should_receive('publish').with_args('hi').once()
module.load_file_preview.__wrapped__(
browse_app=flexmock(),
file_preview_loaded=file_preview_loaded,
config=flexmock(),
repository=flexmock(),
archive_name='archive',
file_path='foo/bar/baz.txt',
)
+11
View File
@@ -22,6 +22,7 @@ def test_run_arbitrary_borg_calls_borg_with_flags():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.run_arbitrary_borg(
repository_path='repo',
@@ -99,6 +100,7 @@ def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_flags():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.run_arbitrary_borg(
repository_path='repo',
@@ -123,6 +125,7 @@ def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_flag():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.run_arbitrary_borg(
repository_path='repo',
@@ -148,6 +151,7 @@ def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
borg_local_path='borg1',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.run_arbitrary_borg(
repository_path='repo',
@@ -174,6 +178,7 @@ def test_run_arbitrary_borg_with_exit_codes_calls_borg_using_them():
borg_local_path='borg',
borg_exit_codes=borg_exit_codes,
)
insert_logging_mock(logging.WARNING)
module.run_arbitrary_borg(
repository_path='repo',
@@ -200,6 +205,7 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_flags()
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.run_arbitrary_borg(
repository_path='repo',
@@ -227,6 +233,7 @@ def test_run_arbitrary_borg_with_remote_path_injection_attack_gets_escaped():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.run_arbitrary_borg(
repository_path='repo',
@@ -252,6 +259,7 @@ def test_run_arbitrary_borg_passes_borg_specific_flags_to_borg():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.run_arbitrary_borg(
repository_path='repo',
@@ -276,6 +284,7 @@ def test_run_arbitrary_borg_omits_dash_dash_in_flags_passed_to_borg():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.run_arbitrary_borg(
repository_path='repo',
@@ -300,6 +309,7 @@ def test_run_arbitrary_borg_without_borg_specific_flags_does_not_raise():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.run_arbitrary_borg(
repository_path='repo',
@@ -376,6 +386,7 @@ def test_run_arbitrary_borg_calls_borg_with_working_directory():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.run_arbitrary_borg(
repository_path='repo',
+8
View File
@@ -24,6 +24,7 @@ def insert_execute_command_mock(command, working_directory=None, borg_exit_codes
def test_break_lock_calls_borg_with_required_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'break-lock', '--log-json', 'repo'))
insert_logging_mock(logging.WARNING)
module.break_lock(
repository_path='repo',
@@ -36,6 +37,7 @@ def test_break_lock_calls_borg_with_required_flags():
def test_break_lock_calls_borg_with_local_path():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg1', 'break-lock', '--log-json', 'repo'))
insert_logging_mock(logging.WARNING)
module.break_lock(
repository_path='repo',
@@ -49,6 +51,7 @@ def test_break_lock_calls_borg_with_local_path():
def test_break_lock_calls_borg_using_exit_codes():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg1', 'break-lock', '--log-json', 'repo'))
insert_logging_mock(logging.WARNING)
module.break_lock(
repository_path='repo',
@@ -64,6 +67,7 @@ def test_break_lock_calls_borg_with_remote_path_flags():
insert_execute_command_mock(
('borg', 'break-lock', '--remote-path', 'borg1', '--log-json', 'repo')
)
insert_logging_mock(logging.WARNING)
module.break_lock(
repository_path='repo',
@@ -77,6 +81,7 @@ def test_break_lock_calls_borg_with_remote_path_flags():
def test_break_lock_calls_borg_with_umask_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'break-lock', '--umask', '0770', '--log-json', 'repo'))
insert_logging_mock(logging.WARNING)
module.break_lock(
repository_path='repo',
@@ -89,6 +94,7 @@ def test_break_lock_calls_borg_with_umask_flags():
def test_break_lock_calls_borg_with_lock_wait_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'break-lock', '--log-json', '--lock-wait', '5', 'repo'))
insert_logging_mock(logging.WARNING)
module.break_lock(
repository_path='repo',
@@ -103,6 +109,7 @@ def test_break_lock_calls_borg_with_extra_borg_options():
insert_execute_command_mock(
('borg', 'break-lock', '--log-json', '--extra', 'value with space', 'repo')
)
insert_logging_mock(logging.WARNING)
module.break_lock(
repository_path='repo',
@@ -145,6 +152,7 @@ def test_break_lock_calls_borg_with_working_directory():
insert_execute_command_mock(
('borg', 'break-lock', '--log-json', 'repo'), working_directory='/working/dir'
)
insert_logging_mock(logging.WARNING)
module.break_lock(
repository_path='repo',
+10
View File
@@ -35,6 +35,7 @@ def insert_execute_command_mock(
def test_change_passphrase_calls_borg_with_required_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'key', 'change-passphrase', 'repo'))
insert_logging_mock(logging.WARNING)
module.change_passphrase(
repository_path='repo',
@@ -48,6 +49,7 @@ def test_change_passphrase_calls_borg_with_required_flags():
def test_change_passphrase_calls_borg_with_local_path():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg1', 'key', 'change-passphrase', 'repo'))
insert_logging_mock(logging.WARNING)
module.change_passphrase(
repository_path='repo',
@@ -68,6 +70,7 @@ def test_change_passphrase_calls_borg_using_exit_codes():
config=config,
borg_exit_codes=borg_exit_codes,
)
insert_logging_mock(logging.WARNING)
module.change_passphrase(
repository_path='repo',
@@ -83,6 +86,7 @@ def test_change_passphrase_calls_borg_with_remote_path_flags():
insert_execute_command_mock(
('borg', 'key', 'change-passphrase', '--remote-path', 'borg1', 'repo'),
)
insert_logging_mock(logging.WARNING)
module.change_passphrase(
repository_path='repo',
@@ -101,6 +105,7 @@ def test_change_passphrase_calls_borg_with_umask_flags():
('borg', 'key', 'change-passphrase', '--umask', '0770', 'repo'),
config=config,
)
insert_logging_mock(logging.WARNING)
module.change_passphrase(
repository_path='repo',
@@ -118,6 +123,7 @@ def test_change_passphrase_calls_borg_with_lock_wait_flags():
('borg', 'key', 'change-passphrase', '--lock-wait', '5', 'repo'),
config=config,
)
insert_logging_mock(logging.WARNING)
module.change_passphrase(
repository_path='repo',
@@ -135,6 +141,7 @@ def test_change_passphrase_calls_borg_with_extra_borg_options():
('borg', 'key', 'change-passphrase', '--extra', 'value with space', 'repo'),
config=config,
)
insert_logging_mock(logging.WARNING)
module.change_passphrase(
repository_path='repo',
@@ -179,6 +186,7 @@ def test_change_passphrase_with_dry_run_skips_borg_call():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.execute).should_receive('execute_command').never()
insert_logging_mock(logging.WARNING)
module.change_passphrase(
repository_path='repo',
@@ -195,6 +203,7 @@ def test_change_passphrase_calls_borg_without_passphrase():
('borg', 'key', 'change-passphrase', 'repo'),
config={'option': 'foo'},
)
insert_logging_mock(logging.WARNING)
module.change_passphrase(
repository_path='repo',
@@ -217,6 +226,7 @@ def test_change_passphrase_calls_borg_with_working_directory():
config=config,
working_directory='/working/dir',
)
insert_logging_mock(logging.WARNING)
module.change_passphrase(
repository_path='repo',
+24 -2
View File
@@ -337,6 +337,7 @@ def test_check_archives_with_progress_passes_through_to_borg():
borg_local_path='borg',
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
).once()
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -373,6 +374,7 @@ def test_check_archives_with_log_json_and_progress_passes_through_both_to_borg()
borg_local_path='borg',
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
).once()
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -409,6 +411,7 @@ def test_check_archives_with_repair_passes_through_to_borg():
borg_local_path='borg',
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
).once()
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -445,6 +448,7 @@ def test_check_archives_with_log_json_and_repair_passes_through_both_to_borg():
borg_local_path='borg',
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
).once()
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -481,6 +485,7 @@ def test_check_archives_with_max_duration_flag_passes_through_to_borg():
borg_local_path='borg',
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
).once()
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -517,6 +522,7 @@ def test_check_archives_with_max_duration_option_passes_through_to_borg():
borg_local_path='borg',
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
).once()
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -550,6 +556,7 @@ def test_check_archives_with_max_duration_option_and_archives_check_runs_reposit
insert_execute_command_mock(
('borg', 'check', '--max-duration', '33', '--repository-only', '--log-json', 'repo'),
)
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -583,6 +590,7 @@ def test_check_archives_with_max_duration_flag_and_archives_check_runs_repositor
insert_execute_command_mock(
('borg', 'check', '--max-duration', '33', '--repository-only', '--log-json', 'repo'),
)
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -619,6 +627,7 @@ def test_check_archives_with_max_duration_option_and_data_check_runs_repository_
insert_execute_command_mock(
('borg', 'check', '--max-duration', '33', '--repository-only', '--log-json', 'repo'),
)
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -655,6 +664,7 @@ def test_check_archives_with_max_duration_flag_and_data_check_runs_repository_ch
insert_execute_command_mock(
('borg', 'check', '--max-duration', '33', '--repository-only', '--log-json', 'repo'),
)
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -691,6 +701,7 @@ def test_check_archives_with_max_duration_flag_overrides_max_duration_option():
borg_local_path='borg',
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
).once()
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -724,6 +735,7 @@ def test_check_archives_calls_borg_with_parameters(checks):
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--log-json', 'repo'))
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -751,6 +763,7 @@ def test_check_archives_with_data_check_implies_archives_check_calls_borg_with_p
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--log-json', 'repo'))
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -777,8 +790,8 @@ def test_check_archives_with_log_info_passes_through_to_borg():
(),
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.INFO)
insert_execute_command_mock(('borg', 'check', '--log-json', '--info', 'repo'))
insert_logging_mock(logging.INFO)
module.check_archives(
repository_path='repo',
@@ -805,8 +818,8 @@ def test_check_archives_with_log_debug_passes_through_to_borg():
(),
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.DEBUG)
insert_execute_command_mock(('borg', 'check', '--log-json', '--debug', '--show-rc', 'repo'))
insert_logging_mock(logging.DEBUG)
module.check_archives(
repository_path='repo',
@@ -832,6 +845,7 @@ def test_check_archives_with_local_path_calls_borg_via_local_path():
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg1', 'check', '--log-json', 'repo'))
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -861,6 +875,7 @@ def test_check_archives_with_exit_codes_calls_borg_using_them():
insert_execute_command_mock(
('borg', 'check', '--log-json', 'repo'), borg_exit_codes=borg_exit_codes
)
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -886,6 +901,7 @@ def test_check_archives_with_remote_path_passes_through_to_borg():
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--remote-path', 'borg1', '--log-json', 'repo'))
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -912,6 +928,7 @@ def test_check_archives_with_umask_passes_through_to_borg():
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--umask', '077', '--log-json', 'repo'))
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -937,6 +954,7 @@ def test_check_archives_with_lock_wait_passes_through_to_borg():
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--log-json', '--lock-wait', '5', 'repo'))
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -963,6 +981,7 @@ def test_check_archives_with_retention_prefix():
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--log-json', 'repo'))
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -992,6 +1011,7 @@ def test_check_archives_with_extra_borg_options_passes_through_to_borg():
insert_execute_command_mock(
('borg', 'check', '--log-json', '--extra', '--options', 'value with space', 'repo'),
)
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -1028,6 +1048,7 @@ def test_check_archives_with_match_archives_passes_through_to_borg():
borg_local_path='borg',
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
).once()
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
@@ -1059,6 +1080,7 @@ def test_check_archives_calls_borg_with_working_directory():
insert_execute_command_mock(
('borg', 'check', '--log-json', 'repo'), working_directory='/working/dir'
)
insert_logging_mock(logging.WARNING)
module.check_archives(
repository_path='repo',
+14
View File
@@ -33,6 +33,7 @@ COMPACT_COMMAND = ('borg', 'compact')
def test_compact_segments_calls_borg_with_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock((*COMPACT_COMMAND, '--log-json', 'repo'), logging.INFO)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
@@ -83,6 +84,7 @@ def test_compact_segments_with_dry_run_skips_borg_call_when_feature_unavailable(
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').never()
flexmock(module).should_receive('execute_command').never()
flexmock(module.logger).should_receive('info').with_args('Skipping compact (dry run)').once()
insert_logging_mock(logging.WARNING)
module.compact_segments(
repository_path='repo',
@@ -102,6 +104,7 @@ def test_compact_segments_with_dry_run_executes_borg_call_when_feature_available
flexmock(module.environment).should_receive('make_environment').once()
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').once()
flexmock(module).should_receive('execute_command').once()
insert_logging_mock(logging.WARNING)
module.compact_segments(
repository_path='repo',
@@ -115,6 +118,7 @@ def test_compact_segments_with_dry_run_executes_borg_call_when_feature_available
def test_compact_segments_with_local_path_calls_borg_via_local_path():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg1', *COMPACT_COMMAND[1:], '--log-json', 'repo'), logging.INFO)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
@@ -134,6 +138,7 @@ def test_compact_segments_with_exit_codes_calls_borg_using_them():
logging.INFO,
borg_exit_codes=borg_exit_codes,
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
@@ -149,6 +154,7 @@ def test_compact_segments_with_remote_path_calls_borg_with_remote_path_flags():
insert_execute_command_mock(
(*COMPACT_COMMAND, '--remote-path', 'borg1', '--log-json', 'repo'), logging.INFO
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
@@ -163,6 +169,7 @@ def test_compact_segments_with_remote_path_calls_borg_with_remote_path_flags():
def test_compact_segments_with_progress_calls_borg_with_progress_flag():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock((*COMPACT_COMMAND, '--progress', 'repo'), logging.INFO)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
@@ -178,6 +185,7 @@ def test_compact_segments_with_log_json_and_progress_calls_borg_with_both_flags(
insert_execute_command_mock(
(*COMPACT_COMMAND, '--log-json', '--progress', 'repo'), logging.INFO
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
@@ -193,6 +201,7 @@ def test_compact_segments_with_cleanup_commits_calls_borg_with_cleanup_commits_f
insert_execute_command_mock(
(*COMPACT_COMMAND, '--log-json', '--cleanup-commits', 'repo'), logging.INFO
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
@@ -209,6 +218,7 @@ def test_compact_segments_with_threshold_calls_borg_with_threshold_flag():
insert_execute_command_mock(
(*COMPACT_COMMAND, '--log-json', '--threshold', '20', 'repo'), logging.INFO
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
@@ -225,6 +235,7 @@ def test_compact_segments_with_umask_calls_borg_with_umask_flags():
insert_execute_command_mock(
(*COMPACT_COMMAND, '--umask', '077', '--log-json', 'repo'), logging.INFO
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
@@ -241,6 +252,7 @@ def test_compact_segments_with_lock_wait_calls_borg_with_lock_wait_flags():
insert_execute_command_mock(
(*COMPACT_COMMAND, '--log-json', '--lock-wait', '5', 'repo'), logging.INFO
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
@@ -257,6 +269,7 @@ def test_compact_segments_with_extra_borg_options_calls_borg_with_extra_options(
(*COMPACT_COMMAND, '--log-json', '--extra', '--options', 'value with space', 'repo'),
logging.INFO,
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
@@ -274,6 +287,7 @@ def test_compact_segments_calls_borg_with_working_directory():
logging.INFO,
working_directory='/working/dir',
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
+86
View File
@@ -1265,6 +1265,7 @@ def test_create_archive_calls_borg_with_flags():
working_directory=None,
environment=None,
)
insert_logging_mock(logging.WARNING)
module.create_archive(
dry_run=False,
@@ -1299,6 +1300,7 @@ def test_create_archive_calls_borg_with_environment():
working_directory=None,
environment=environment,
)
insert_logging_mock(logging.WARNING)
module.create_archive(
dry_run=False,
@@ -1477,6 +1479,44 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats():
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
'statistics': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
global_arguments=flexmock(),
borgmatic_runtime_directory='/borgmatic/run',
)
def test_create_archive_with_quick_stats_and_dry_run_calls_borg_without_quick_stats():
# --dry-run and --quick-stats are mutually exclusive, see:
# https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_base_create_command').and_return(
(('borg', 'create', '--dry-run'), REPO_ARCHIVE, flexmock()),
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--dry-run', '--info', *REPO_ARCHIVE),
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
environment=None,
)
insert_logging_mock(logging.INFO)
module.create_archive(
dry_run=True,
repository_path='repo',
config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
'quick_statistics': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
@@ -1504,6 +1544,7 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
working_directory='/working/dir',
environment=None,
)
insert_logging_mock(logging.WARNING)
module.create_archive(
dry_run=False,
@@ -1539,6 +1580,7 @@ def test_create_archive_with_exit_codes_calls_borg_using_them():
working_directory=None,
environment=None,
)
insert_logging_mock(logging.WARNING)
module.create_archive(
dry_run=False,
@@ -1573,6 +1615,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_flag_and_answer_output_
working_directory=None,
environment=None,
)
insert_logging_mock(logging.WARNING)
module.create_archive(
dry_run=False,
@@ -1611,6 +1654,7 @@ def test_create_archive_with_files_calls_borg_with_answer_output_log_level():
working_directory=None,
environment=None,
)
insert_logging_mock(logging.WARNING)
module.create_archive(
dry_run=False,
@@ -1680,6 +1724,7 @@ def test_create_archive_with_progress_calls_borg_with_progress_flag():
working_directory=None,
environment=None,
)
insert_logging_mock(logging.WARNING)
module.create_archive(
dry_run=False,
@@ -1731,6 +1776,7 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
working_directory=None,
environment=None,
).and_yield()
insert_logging_mock(logging.WARNING)
module.create_archive(
dry_run=False,
@@ -1764,6 +1810,7 @@ def test_create_archive_with_json_calls_borg_with_json_flag():
borg_local_path='borg',
borg_exit_codes=None,
).and_yield('[]')
insert_logging_mock(logging.WARNING)
json_output = module.create_archive(
dry_run=False,
@@ -1798,6 +1845,7 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
borg_local_path='borg',
borg_exit_codes=None,
).and_yield('[]')
insert_logging_mock(logging.WARNING)
json_output = module.create_archive(
dry_run=False,
@@ -1806,6 +1854,42 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
'source_directories': ['foo*'],
'repositories': ['repo'],
'exclude_patterns': None,
'statistics': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
global_arguments=flexmock(),
borgmatic_runtime_directory='/borgmatic/run',
json=True,
)
assert json_output == '[]'
def test_create_archive_with_quick_stats_and_json_calls_borg_without_quick_stats_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_base_create_command').and_return(
(('borg', 'create'), REPO_ARCHIVE, flexmock()),
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'create', '--json', *REPO_ARCHIVE),
working_directory=None,
environment=None,
borg_local_path='borg',
borg_exit_codes=None,
).and_yield('[]')
json_output = module.create_archive(
dry_run=False,
repository_path='repo',
config={
'source_directories': ['foo*'],
'repositories': ['repo'],
'exclude_patterns': None,
'quick_statistics': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
@@ -1834,6 +1918,7 @@ def test_create_archive_with_comment_calls_borg_with_comment_flag():
working_directory=None,
environment=None,
)
insert_logging_mock(logging.WARNING)
module.create_archive(
dry_run=False,
@@ -1871,6 +1956,7 @@ def test_create_archive_calls_borg_with_working_directory():
working_directory='/working/dir',
environment=None,
)
insert_logging_mock(logging.WARNING)
module.create_archive(
dry_run=False,
+17 -2
View File
@@ -9,13 +9,13 @@ from ..test_verbosity import insert_logging_mock
def test_make_delete_command_includes_log_info():
insert_logging_mock(logging.INFO)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.INFO)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -31,13 +31,13 @@ def test_make_delete_command_includes_log_info():
def test_make_delete_command_includes_log_debug():
insert_logging_mock(logging.DEBUG)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.DEBUG)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -63,6 +63,7 @@ def test_make_delete_command_includes_dry_run():
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.WARNING)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -88,6 +89,7 @@ def test_make_delete_command_includes_remote_path():
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.WARNING)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -111,6 +113,7 @@ def test_make_delete_command_includes_umask():
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.WARNING)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -136,6 +139,7 @@ def test_make_delete_command_includes_lock_wait():
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.WARNING)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -157,6 +161,7 @@ def test_make_delete_command_includes_extra_borg_options():
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.WARNING)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -182,6 +187,7 @@ def test_make_delete_command_with_list_config_calls_borg_with_list_flag():
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.WARNING)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -203,6 +209,7 @@ def test_make_delete_command_includes_force():
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.WARNING)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -224,6 +231,7 @@ def test_make_delete_command_includes_force_twice():
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.WARNING)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -247,6 +255,7 @@ def test_make_delete_command_includes_archive():
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.WARNING)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -275,6 +284,7 @@ def test_make_delete_command_includes_match_archives():
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
insert_logging_mock(logging.WARNING)
command = module.make_delete_command(
repository={'path': 'repo'},
@@ -307,6 +317,7 @@ def test_delete_archives_with_archive_calls_borg_delete():
)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.execute).should_receive('execute_command').once()
insert_logging_mock(logging.WARNING)
module.delete_archives(
repository={'path': 'repo'},
@@ -327,6 +338,7 @@ def test_delete_archives_with_match_archives_calls_borg_delete():
)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.execute).should_receive('execute_command').once()
insert_logging_mock(logging.WARNING)
module.delete_archives(
repository={'path': 'repo'},
@@ -348,6 +360,7 @@ def test_delete_archives_with_archive_related_argument_calls_borg_delete(argumen
)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.execute).should_receive('execute_command').once()
insert_logging_mock(logging.WARNING)
module.delete_archives(
repository={'path': 'repo'},
@@ -367,6 +380,7 @@ def test_delete_archives_without_archive_related_argument_calls_borg_repo_delete
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').never()
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.execute).should_receive('execute_command').never()
insert_logging_mock(logging.WARNING)
module.delete_archives(
repository={'path': 'repo'},
@@ -403,6 +417,7 @@ def test_delete_archives_calls_borg_delete_with_working_directory():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.delete_archives(
repository={'path': 'repo'},
+17
View File
@@ -4,6 +4,8 @@ from flexmock import flexmock
from borgmatic.borg import diff as module
from ..test_verbosity import insert_logging_mock
LOGGING_ANSWER = flexmock()
@@ -40,6 +42,7 @@ def test_diff_calls_borg_with_archives():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -94,6 +97,7 @@ def test_diff_with_local_path_calls_borg_with_it():
borg_local_path='borg6',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -150,6 +154,7 @@ def test_diff_with_remote_path_calls_borg_with_it():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -206,6 +211,7 @@ def test_diff_with_lock_wait_calls_borg_with_it():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -262,6 +268,7 @@ def test_diff_with_log_level_info_calls_borg_with_info_flag():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -319,6 +326,7 @@ def test_diff_with_log_level_debug_calls_borg_with_debug_flags():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -376,6 +384,7 @@ def test_diff_with_only_patterns_calls_borg_with_configured_pattern_paths():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -434,6 +443,7 @@ def test_diff_with_exclude_config_calls_borg_with_exclude_flags():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -489,6 +499,7 @@ def test_diff_with_numeric_ids_calls_borg_with_numeric_ids_flag():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -549,6 +560,7 @@ def test_diff_with_numeric_ids_and_feature_not_available_calls_borg_with_numeric
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -604,6 +616,7 @@ def test_diff_with_same_chunker_params_calls_borg_with_it():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -660,6 +673,7 @@ def test_diff_with_sort_keys_calls_borg_with_formatted_sort_by_flags():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -715,6 +729,7 @@ def test_diff_with_content_only_calls_borg_with_it():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -771,6 +786,7 @@ def test_diff_with_extra_borg_options_calls_borg_with_them():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
@@ -828,6 +844,7 @@ def test_diff_without_separate_repository_archive_feature_available_calls_borg_j
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
+14
View File
@@ -36,6 +36,7 @@ def test_export_key_calls_borg_with_required_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'export', 'repo'))
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -50,6 +51,7 @@ def test_export_key_calls_borg_with_local_path():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg1', 'key', 'export', 'repo'))
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -66,6 +68,7 @@ def test_export_key_calls_borg_using_exit_codes():
flexmock(module.os.path).should_receive('exists').never()
borg_exit_codes = flexmock()
insert_execute_command_mock(('borg', 'key', 'export', 'repo'), borg_exit_codes=borg_exit_codes)
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -80,6 +83,7 @@ def test_export_key_calls_borg_with_remote_path_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'export', '--remote-path', 'borg1', 'repo'))
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -95,6 +99,7 @@ def test_export_key_calls_borg_with_umask_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'export', '--umask', '0770', 'repo'))
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -109,6 +114,7 @@ def test_export_key_calls_borg_with_lock_wait_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'export', '--lock-wait', '5', 'repo'))
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -123,6 +129,7 @@ def test_export_key_calls_borg_with_extra_borg_options():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'export', '--extra', 'value with space', 'repo'))
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -167,6 +174,7 @@ def test_export_key_calls_borg_with_paper_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'export', '--paper', 'repo'))
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -181,6 +189,7 @@ def test_export_key_calls_borg_with_paper_flag():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'export', '--paper', 'repo'))
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -195,6 +204,7 @@ def test_export_key_calls_borg_with_qr_html_flag():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'export', '--qr-html', 'repo'))
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -211,6 +221,7 @@ def test_export_key_calls_borg_with_path_argument():
insert_execute_command_mock(
('borg', 'key', 'export', '--log-json', 'repo', 'dest'), output_file=None
)
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -240,6 +251,7 @@ def test_export_key_with_stdout_path_calls_borg_without_path_argument():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'export', 'repo'))
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -268,6 +280,7 @@ def test_export_key_calls_borg_with_working_directory():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'export', 'repo'), working_directory='/working/dir')
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
@@ -288,6 +301,7 @@ def test_export_key_calls_borg_with_path_argument_and_working_directory():
output_file=None,
working_directory='/working/dir',
)
insert_logging_mock(logging.WARNING)
module.export_key(
repository_path='repo',
+13
View File
@@ -39,6 +39,7 @@ def test_export_tar_archive_calls_borg_with_path_flags():
insert_execute_command_mock(
('borg', 'export-tar', '--log-json', 'repo::archive', 'test.tar', 'path1', 'path2'),
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -62,6 +63,7 @@ def test_export_tar_archive_calls_borg_with_local_path_flags():
('borg1', 'export-tar', '--log-json', 'repo::archive', 'test.tar'),
borg_local_path='borg1',
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -87,6 +89,7 @@ def test_export_tar_archive_calls_borg_using_exit_codes():
('borg', 'export-tar', '--log-json', 'repo::archive', 'test.tar'),
borg_exit_codes=borg_exit_codes,
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -109,6 +112,7 @@ def test_export_tar_archive_calls_borg_with_remote_path_flags():
insert_execute_command_mock(
('borg', 'export-tar', '--remote-path', 'borg1', '--log-json', 'repo::archive', 'test.tar'),
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -132,6 +136,7 @@ def test_export_tar_archive_calls_borg_with_umask_flags():
insert_execute_command_mock(
('borg', 'export-tar', '--umask', '0770', '--log-json', 'repo::archive', 'test.tar'),
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -154,6 +159,7 @@ def test_export_tar_archive_calls_borg_with_lock_wait_flags():
insert_execute_command_mock(
('borg', 'export-tar', '--log-json', '--lock-wait', '5', 'repo::archive', 'test.tar'),
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -184,6 +190,7 @@ def test_export_tar_archive_calls_borg_with_extra_borg_options():
'test.tar',
),
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -272,6 +279,7 @@ def test_export_tar_archive_calls_borg_with_tar_filter_flags():
insert_execute_command_mock(
('borg', 'export-tar', '--log-json', '--tar-filter', 'bzip2', 'repo::archive', 'test.tar'),
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -296,6 +304,7 @@ def test_export_tar_archive_calls_borg_with_list_flag():
('borg', 'export-tar', '--log-json', '--list', 'repo::archive', 'test.tar'),
output_log_level=logging.ANSWER,
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -326,6 +335,7 @@ def test_export_tar_archive_calls_borg_with_strip_components_flag():
'test.tar',
),
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -349,6 +359,7 @@ def test_export_tar_archive_skips_abspath_for_remote_repository_flag():
insert_execute_command_mock(
('borg', 'export-tar', '--log-json', 'server:repo::archive', 'test.tar')
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -369,6 +380,7 @@ def test_export_tar_archive_calls_borg_with_stdout_destination_path():
('repo::archive',),
)
insert_execute_command_mock(('borg', 'export-tar', 'repo::archive', '-'), capture=False)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
@@ -392,6 +404,7 @@ def test_export_tar_archive_calls_borg_with_working_directory():
('borg', 'export-tar', '--log-json', 'repo::archive', 'test.tar'),
working_directory='/working/dir',
)
insert_logging_mock(logging.WARNING)
module.export_tar_archive(
dry_run=False,
+33 -4
View File
@@ -25,6 +25,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_last_archive():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',),
)
insert_logging_mock(logging.WARNING)
module.extract_last_archive_dry_run(
config={},
@@ -38,6 +39,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_last_archive():
def test_extract_last_archive_dry_run_without_any_archives_should_not_raise():
flexmock(module.repo_list).should_receive('resolve_archive_name').and_raise(ValueError)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
module.extract_last_archive_dry_run(
config={},
@@ -53,10 +55,10 @@ def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_paramet
insert_execute_command_mock(
('borg', 'extract', '--dry-run', '--log-json', '--info', 'repo::archive')
)
insert_logging_mock(logging.INFO)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',),
)
insert_logging_mock(logging.INFO)
module.extract_last_archive_dry_run(
config={},
@@ -81,10 +83,10 @@ def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_param
'repo::archive',
),
)
insert_logging_mock(logging.DEBUG)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',),
)
insert_logging_mock(logging.DEBUG)
module.extract_last_archive_dry_run(
config={},
@@ -109,6 +111,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_progress_flag():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',),
)
insert_logging_mock(logging.WARNING)
module.extract_last_archive_dry_run(
config={'progress': True},
@@ -125,6 +128,7 @@ def test_extract_last_archive_dry_run_calls_borg_via_local_path():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',),
)
insert_logging_mock(logging.WARNING)
module.extract_last_archive_dry_run(
config={},
@@ -146,6 +150,7 @@ def test_extract_last_archive_dry_run_calls_borg_using_exit_codes():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',),
)
insert_logging_mock(logging.WARNING)
module.extract_last_archive_dry_run(
config={'borg_exit_codes': borg_exit_codes},
@@ -164,6 +169,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_remote_path_flags():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',),
)
insert_logging_mock(logging.WARNING)
module.extract_last_archive_dry_run(
config={},
@@ -183,6 +189,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_flags():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',),
)
insert_logging_mock(logging.WARNING)
module.extract_last_archive_dry_run(
config={},
@@ -209,6 +216,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_extra_borg_options():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',),
)
insert_logging_mock(logging.WARNING)
module.extract_last_archive_dry_run(
config={'extra_borg_options': {'extract': '--extra "value with space"'}},
@@ -231,6 +239,7 @@ def test_extract_archive_calls_borg_with_path_flags():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -254,6 +263,7 @@ def test_extract_archive_calls_borg_with_local_path():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -282,6 +292,7 @@ def test_extract_archive_calls_borg_with_exit_codes():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -307,6 +318,7 @@ def test_extract_archive_calls_borg_with_remote_path_flags():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -338,6 +350,7 @@ def test_extract_archive_calls_borg_with_numeric_ids_parameter(feature_available
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -363,6 +376,7 @@ def test_extract_archive_calls_borg_with_umask_flags():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -388,6 +402,7 @@ def test_extract_archive_calls_borg_with_lock_wait_flags():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -413,6 +428,7 @@ def test_extract_archive_calls_borg_with_extra_borg_options():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -428,7 +444,6 @@ def test_extract_archive_calls_borg_with_extra_borg_options():
def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--log-json', '--info', 'repo::archive'))
insert_logging_mock(logging.INFO)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
@@ -437,6 +452,7 @@ def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.INFO)
module.extract_archive(
dry_run=False,
@@ -454,7 +470,6 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_flags():
insert_execute_command_mock(
('borg', 'extract', '--log-json', '--debug', '--list', '--show-rc', 'repo::archive'),
)
insert_logging_mock(logging.DEBUG)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
@@ -463,6 +478,7 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_flags():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.DEBUG)
module.extract_archive(
dry_run=False,
@@ -486,6 +502,7 @@ def test_extract_archive_calls_borg_with_dry_run_parameter():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=True,
@@ -511,6 +528,7 @@ def test_extract_archive_calls_borg_with_destination_path():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -537,6 +555,7 @@ def test_extract_archive_calls_borg_with_strip_components():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -572,6 +591,7 @@ def test_extract_archive_calls_borg_with_strip_components_calculated_from_all():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -607,6 +627,7 @@ def test_extract_archive_calls_borg_with_strip_components_calculated_from_all_wi
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -631,6 +652,7 @@ def test_extract_archive_with_strip_components_all_and_no_paths_raises():
'normalize_repository_path',
).and_return('repo')
flexmock(module).should_receive('execute_command').never()
insert_logging_mock(logging.WARNING)
with pytest.raises(ValueError):
module.extract_archive(
@@ -665,6 +687,7 @@ def test_extract_archive_calls_borg_with_progress_flag():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -697,6 +720,7 @@ def test_extract_archive_with_log_json_and_progress_calls_borg_with_both_flags()
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -731,6 +755,7 @@ def test_extract_archive_calls_borg_with_extract_to_stdout_returns_process():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
assert (
module.extract_archive(
@@ -769,6 +794,7 @@ def test_extract_archive_with_progress_and_extract_to_stdout_ignores_progress():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
assert (
module.extract_archive(
@@ -804,6 +830,7 @@ def test_extract_archive_skips_abspath_for_remote_repository():
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).and_return('repo')
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -829,6 +856,7 @@ def test_extract_archive_uses_configured_working_directory_in_repo_path_and_dest
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).with_args('repo', '/working/dir').and_return('/working/dir/repo').once()
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
@@ -852,6 +880,7 @@ def test_extract_archive_uses_configured_working_directory_in_repo_path_when_des
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path',
).with_args('repo', '/working/dir').and_return('/working/dir/repo').once()
insert_logging_mock(logging.WARNING)
module.extract_archive(
dry_run=False,
+12
View File
@@ -32,6 +32,7 @@ def test_import_key_calls_borg_with_required_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'import', '--log-json', 'repo', '-'))
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
@@ -47,6 +48,7 @@ def test_import_key_calls_borg_with_local_path():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg1', 'key', 'import', '--log-json', 'repo', '-'))
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
@@ -66,6 +68,7 @@ def test_import_key_calls_borg_using_exit_codes():
insert_execute_command_mock(
('borg', 'key', 'import', '--log-json', 'repo', '-'), borg_exit_codes=borg_exit_codes
)
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
@@ -83,6 +86,7 @@ def test_import_key_calls_borg_with_remote_path_flags():
insert_execute_command_mock(
('borg', 'key', 'import', '--remote-path', 'borg1', '--log-json', 'repo', '-')
)
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
@@ -101,6 +105,7 @@ def test_import_key_calls_borg_with_umask_flags():
insert_execute_command_mock(
('borg', 'key', 'import', '--umask', '0770', '--log-json', 'repo', '-')
)
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
@@ -118,6 +123,7 @@ def test_import_key_calls_borg_with_lock_wait_flags():
insert_execute_command_mock(
('borg', 'key', 'import', '--log-json', '--lock-wait', '5', 'repo', '-')
)
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
@@ -135,6 +141,7 @@ def test_import_key_calls_borg_with_extra_borg_options():
insert_execute_command_mock(
('borg', 'key', 'import', '--log-json', '--extra', 'value with space', 'repo', '-')
)
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
@@ -184,6 +191,7 @@ def test_import_key_calls_borg_with_paper_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'import', '--log-json', '--paper', 'repo', '-'))
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
@@ -201,6 +209,7 @@ def test_import_key_calls_borg_with_path_argument():
insert_execute_command_mock(
('borg', 'key', 'import', '--log-json', 'repo', 'source'),
)
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
@@ -232,6 +241,7 @@ def test_import_key_with_stdin_path_calls_borg_without_path_argument():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.os.path).should_receive('exists').never()
insert_execute_command_mock(('borg', 'key', 'import', '--log-json', 'repo', '-'))
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
@@ -264,6 +274,7 @@ def test_import_key_calls_borg_with_working_directory():
insert_execute_command_mock(
('borg', 'key', 'import', '--log-json', 'repo', '-'), working_directory='/working/dir'
)
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
@@ -284,6 +295,7 @@ def test_import_key_calls_borg_with_path_argument_and_working_directory():
('borg', 'key', 'import', '--log-json', 'repo', 'source'),
working_directory='/working/dir',
)
insert_logging_mock(logging.WARNING)
module.import_key(
repository_path='repo',
+16
View File
@@ -17,6 +17,7 @@ def test_make_info_command_constructs_borg_info_command():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -112,6 +113,7 @@ def test_make_info_command_with_log_debug_and_json_omits_borg_logging_flags():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -135,6 +137,7 @@ def test_make_info_command_with_json_passes_through_to_command():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -158,6 +161,7 @@ def test_make_info_command_with_archive_uses_match_archives_flags():
).and_return(('--match-archives', 'archive'))
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -189,6 +193,7 @@ def test_make_info_command_with_local_path_passes_through_to_command():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -216,6 +221,7 @@ def test_make_info_command_with_remote_path_passes_through_to_command():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -241,6 +247,7 @@ def test_make_info_command_with_umask_passes_through_to_command():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -268,6 +275,7 @@ def test_make_info_command_with_lock_wait_passes_through_to_command():
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
config = {'lock_wait': 5}
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -292,6 +300,7 @@ def test_make_info_command_with_extra_borg_options_passes_through_to_command():
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
config = {'extra_borg_options': {'info': '--extra "value with space"'}}
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -327,6 +336,7 @@ def test_make_info_command_transforms_prefix_into_match_archives_flags():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -362,6 +372,7 @@ def test_make_info_command_prefers_prefix_over_archive_name_format():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -393,6 +404,7 @@ def test_make_info_command_transforms_archive_name_format_into_match_archives_fl
).and_return(('--match-archives', 'sh:bar-*'))
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -425,6 +437,7 @@ def test_make_info_command_with_match_archives_option_passes_through_to_command(
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
flexmock(module.environment).should_receive('make_environment')
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -460,6 +473,7 @@ def test_make_info_command_with_match_archives_flag_passes_through_to_command():
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
flexmock(module.environment).should_receive('make_environment')
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -496,6 +510,7 @@ def test_make_info_command_passes_arguments_through_to_command(argument_name):
)
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
flexmock(module.environment).should_receive('make_environment')
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
@@ -537,6 +552,7 @@ def test_make_info_command_with_date_based_matching_passes_through_to_command():
older='1m',
oldest='1w',
)
insert_logging_mock(logging.WARNING)
command = module.make_info_command(
repository_path='repo',
+11
View File
@@ -78,6 +78,7 @@ def test_make_list_command_includes_json_but_not_debug():
def test_make_list_command_includes_json():
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
@@ -94,6 +95,7 @@ def test_make_list_command_includes_json():
def test_make_list_command_includes_lock_wait():
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
('--lock-wait', '5'),
).and_return(())
@@ -112,6 +114,7 @@ def test_make_list_command_includes_lock_wait():
def test_make_list_command_includes_format():
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
()
).and_return(('--format', 'stuff'))
@@ -130,6 +133,7 @@ def test_make_list_command_includes_format():
def test_make_list_command_includes_extra_borg_options():
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
@@ -146,6 +150,7 @@ def test_make_list_command_includes_extra_borg_options():
def test_make_list_command_includes_archive():
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
@@ -164,6 +169,7 @@ def test_make_list_command_includes_archive():
def test_make_list_command_includes_archive_and_path():
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
@@ -182,6 +188,7 @@ def test_make_list_command_includes_archive_and_path():
def test_make_list_command_includes_local_path():
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
@@ -199,6 +206,7 @@ def test_make_list_command_includes_local_path():
def test_make_list_command_includes_remote_path():
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags').with_args(
'remote-path',
@@ -223,6 +231,7 @@ def test_make_list_command_includes_remote_path():
def test_make_list_command_includes_umask():
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').replace_with(
lambda name, value: (f'--{name}', value) if value else (),
)
@@ -241,6 +250,7 @@ def test_make_list_command_includes_umask():
def test_make_list_command_includes_short():
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
@@ -271,6 +281,7 @@ def test_make_list_command_includes_short():
),
)
def test_make_list_command_includes_additional_flags(argument_name):
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
(f"--{argument_name.replace('_', '-')}", 'value'),
+14
View File
@@ -25,6 +25,7 @@ def test_mount_archive_calls_borg_with_required_flags():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'mount', '--log-json', 'repo', '/mnt'))
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
module.mount_archive(
@@ -48,6 +49,7 @@ def test_mount_archive_with_borg_features_calls_borg_with_repository_and_match_a
insert_execute_command_mock(
('borg', 'mount', '--log-json', '--repo', 'repo', '--match-archives', 'archive', '/mnt'),
)
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
module.mount_archive(
@@ -66,6 +68,7 @@ def test_mount_archive_without_archive_calls_borg_with_repository_flags_only():
('repo::archive',),
)
insert_execute_command_mock(('borg', 'mount', '--log-json', 'repo::archive', '/mnt'))
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
module.mount_archive(
@@ -86,6 +89,7 @@ def test_mount_archive_calls_borg_with_path_flags():
insert_execute_command_mock(
('borg', 'mount', '--log-json', 'repo::archive', '/mnt', 'path1', 'path2')
)
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(
mount_point='/mnt',
@@ -109,6 +113,7 @@ def test_mount_archive_calls_borg_with_local_path():
('repo::archive',),
)
insert_execute_command_mock(('borg1', 'mount', '--log-json', 'repo::archive', '/mnt'))
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
module.mount_archive(
@@ -132,6 +137,7 @@ def test_mount_archive_calls_borg_using_exit_codes():
('borg', 'mount', '--log-json', 'repo::archive', '/mnt'),
borg_exit_codes=borg_exit_codes,
)
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
module.mount_archive(
@@ -152,6 +158,7 @@ def test_mount_archive_calls_borg_with_remote_path_flags():
insert_execute_command_mock(
('borg', 'mount', '--remote-path', 'borg1', '--log-json', 'repo::archive', '/mnt'),
)
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
module.mount_archive(
@@ -173,6 +180,7 @@ def test_mount_archive_calls_borg_with_umask_flags():
insert_execute_command_mock(
('borg', 'mount', '--umask', '0770', '--log-json', 'repo::archive', '/mnt')
)
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
module.mount_archive(
@@ -193,6 +201,7 @@ def test_mount_archive_calls_borg_with_lock_wait_flags():
insert_execute_command_mock(
('borg', 'mount', '--log-json', '--lock-wait', '5', 'repo::archive', '/mnt')
)
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
module.mount_archive(
@@ -213,6 +222,7 @@ def test_mount_archive_calls_borg_with_extra_borg_options():
insert_execute_command_mock(
('borg', 'mount', '--log-json', '--extra', 'value with space', 'repo::archive', '/mnt')
)
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
module.mount_archive(
@@ -280,6 +290,7 @@ def test_mount_archive_calls_borg_with_foreground_parameter():
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=True)
module.mount_archive(
@@ -300,6 +311,7 @@ def test_mount_archive_calls_borg_with_options_flags():
insert_execute_command_mock(
('borg', 'mount', '--log-json', '-o', 'super_mount', 'repo::archive', '/mnt')
)
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(
mount_point='/mnt',
@@ -361,6 +373,7 @@ def test_mount_archive_with_date_based_matching_calls_borg_with_date_based_flags
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(
mount_point='/mnt',
@@ -388,6 +401,7 @@ def test_mount_archive_calls_borg_with_working_directory():
insert_execute_command_mock(
('borg', 'mount', '--log-json', 'repo', '/mnt'), working_directory='/working/dir'
)
insert_logging_mock(logging.WARNING)
mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
module.mount_archive(
+13
View File
@@ -244,6 +244,7 @@ def test_prune_archives_calls_borg_with_flags():
'1.2.3',
).and_return(False)
insert_execute_command_mock((*PRUNE_COMMAND, 'repo'), logging.INFO)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
@@ -312,6 +313,7 @@ def test_prune_archives_with_dry_run_calls_borg_with_dry_run_flag():
'1.2.3',
).and_return(False)
insert_execute_command_mock((*PRUNE_COMMAND, '--dry-run', 'repo'), logging.INFO)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
@@ -334,6 +336,7 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path():
'1.2.3',
).and_return(False)
insert_execute_command_mock(('borg1', *PRUNE_COMMAND[1:], 'repo'), logging.INFO)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
@@ -362,6 +365,7 @@ def test_prune_archives_with_exit_codes_calls_borg_using_them():
logging.INFO,
borg_exit_codes=borg_exit_codes,
)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
@@ -384,6 +388,7 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_flags():
'1.2.3',
).and_return(False)
insert_execute_command_mock((*PRUNE_COMMAND, '--remote-path', 'borg1', 'repo'), logging.INFO)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
@@ -407,6 +412,7 @@ def test_prune_archives_with_stats_config_calls_borg_with_stats_flag():
'1.2.3',
).and_return(False)
insert_execute_command_mock((*PRUNE_COMMAND, '--stats', 'repo'), module.borgmatic.logger.ANSWER)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=None, list_details=False)
module.prune_archives(
@@ -429,6 +435,7 @@ def test_prune_archives_with_list_config_calls_borg_with_list_flag():
'1.2.3',
).and_return(False)
insert_execute_command_mock((*PRUNE_COMMAND, '--list', 'repo'), module.borgmatic.logger.ANSWER)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=False, list_details=None)
module.prune_archives(
@@ -452,6 +459,7 @@ def test_prune_archives_with_umask_calls_borg_with_umask_flags():
'1.2.3',
).and_return(False)
insert_execute_command_mock((*PRUNE_COMMAND, '--umask', '077', 'repo'), logging.INFO)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
@@ -475,6 +483,7 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_flags():
'1.2.3',
).and_return(False)
insert_execute_command_mock((*PRUNE_COMMAND, '--lock-wait', '5', 'repo'), logging.INFO)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
@@ -500,6 +509,7 @@ def test_prune_archives_with_extra_borg_options_calls_borg_with_extra_options():
(*PRUNE_COMMAND, '--extra', '--options', 'value with space', 'repo'),
logging.INFO,
)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
@@ -569,6 +579,7 @@ def test_prune_archives_with_date_based_matching_calls_borg_with_date_based_flag
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(
statistics=False,
@@ -602,6 +613,7 @@ def test_prune_archives_calls_borg_with_working_directory():
logging.INFO,
working_directory='/working/dir',
)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
@@ -624,6 +636,7 @@ def test_prune_archives_calls_borg_without_stats_when_feature_is_not_available()
'2.0.0b10',
).and_return(True)
insert_execute_command_mock((*PRUNE_COMMAND, 'repo'), logging.ANSWER)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=True, list_details=False)
module.prune_archives(
+21 -1
View File
@@ -35,6 +35,7 @@ def test_recreate_calls_borg_with_required_flags():
),
)
insert_execute_command_mock(('borg', 'recreate', '--log-json', '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -70,6 +71,7 @@ def test_recreate_with_dry_run_calls_borg_with_dry_run_flag():
),
)
insert_execute_command_mock(('borg', 'recreate', '--log-json', '--dry-run', '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -107,6 +109,7 @@ def test_recreate_with_remote_path():
insert_execute_command_mock(
('borg', 'recreate', '--remote-path', 'borg1', '--log-json', '--repo', 'repo')
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -144,6 +147,7 @@ def test_recreate_with_lock_wait():
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--lock-wait', '5', '--repo', 'repo')
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -180,6 +184,7 @@ def test_recreate_with_extra_borg_options():
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--extra', 'value with space', '--repo', 'repo')
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -214,7 +219,6 @@ def test_recreate_with_log_info():
),
)
insert_execute_command_mock(('borg', 'recreate', '--log-json', '--info', '--repo', 'repo'))
insert_logging_mock(logging.INFO)
module.recreate_archive(
@@ -291,6 +295,7 @@ def test_recreate_with_list_config_calls_borg_with_list_flag():
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--list', '--filter', 'AME+-', '--repo', 'repo'),
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -328,6 +333,7 @@ def test_recreate_with_patterns_from_flag():
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--patterns-from', 'patterns_file', '--repo', 'repo'),
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -366,6 +372,7 @@ def test_recreate_with_exclude_flags():
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--exclude', 'pattern', '--repo', 'repo')
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -402,6 +409,7 @@ def test_recreate_with_target_flag():
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--target', 'new-archive', '--repo', 'repo')
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -446,6 +454,7 @@ def test_recreate_with_comment_flag():
'repo',
),
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -482,6 +491,7 @@ def test_recreate_with_timestamp_flag():
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--timestamp', '2023-10-01T12:00:00', '--repo', 'repo'),
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -518,6 +528,7 @@ def test_recreate_with_compression_flag():
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--compression', 'lz4', '--repo', 'repo')
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -554,6 +565,7 @@ def test_recreate_with_chunker_params_flag():
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--chunker-params', '19,23,21,4095', '--repo', 'repo'),
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -590,6 +602,7 @@ def test_recreate_with_recompress_flag():
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--recompress', 'always', '--repo', 'repo')
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -624,6 +637,7 @@ def test_recreate_with_match_archives_star():
),
)
insert_execute_command_mock(('borg', 'recreate', '--log-json', '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -658,6 +672,7 @@ def test_recreate_with_match_archives_regex():
),
)
insert_execute_command_mock(('borg', 'recreate', '--log-json', '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -692,6 +707,7 @@ def test_recreate_with_match_archives_shell():
),
)
insert_execute_command_mock(('borg', 'recreate', '--log-json', '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -728,6 +744,7 @@ def test_recreate_with_match_archives_and_feature_available_calls_borg_with_matc
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--repo', 'repo', '--match-archives', 'foo-*')
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -764,6 +781,7 @@ def test_recreate_with_archives_flag_and_feature_available_calls_borg_with_match
insert_execute_command_mock(
('borg', 'recreate', '--log-json', '--repo', 'repo', '--match-archives', 'archive'),
)
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -794,6 +812,7 @@ def test_recreate_with_match_archives_and_feature_not_available_calls_borg_witho
)
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
insert_execute_command_mock(('borg', 'recreate', '--log-json', 'repo'))
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
@@ -824,6 +843,7 @@ def test_recreate_with_archives_flags_and_feature_not_available_calls_borg_with_
).and_return(('repo::archive',))
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').never()
insert_execute_command_mock(('borg', 'recreate', '--log-json', 'repo::archive'))
insert_logging_mock(logging.WARNING)
module.recreate_archive(
repository='repo',
+15
View File
@@ -50,6 +50,7 @@ def insert_repo_create_command_mock(
def test_create_repository_calls_borg_with_flags():
insert_repo_info_command_not_found_mock()
insert_repo_create_command_mock((*REPO_CREATE_COMMAND, '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -177,6 +178,7 @@ def test_create_repository_with_source_repository_calls_borg_with_other_repo_fla
insert_repo_create_command_mock(
(*REPO_CREATE_COMMAND, '--other-repo', 'other.borg', '--repo', 'repo'),
)
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -199,6 +201,7 @@ def test_create_repository_with_source_repository_calls_borg_with_other_repo_fla
def test_create_repository_with_copy_crypt_key_calls_borg_with_copy_crypt_key_flag():
insert_repo_info_command_not_found_mock()
insert_repo_create_command_mock((*REPO_CREATE_COMMAND, '--copy-crypt-key', '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -221,6 +224,7 @@ def test_create_repository_with_copy_crypt_key_calls_borg_with_copy_crypt_key_fl
def test_create_repository_with_append_only_calls_borg_with_append_only_flag():
insert_repo_info_command_not_found_mock()
insert_repo_create_command_mock((*REPO_CREATE_COMMAND, '--append-only', '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -243,6 +247,7 @@ def test_create_repository_with_append_only_calls_borg_with_append_only_flag():
def test_create_repository_with_append_only_config_calls_borg_with_append_only_flag():
insert_repo_info_command_not_found_mock()
insert_repo_create_command_mock((*REPO_CREATE_COMMAND, '--append-only', '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -267,6 +272,7 @@ def test_create_repository_with_storage_quota_calls_borg_with_storage_quota_flag
insert_repo_create_command_mock(
(*REPO_CREATE_COMMAND, '--storage-quota', '5G', '--repo', 'repo'),
)
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -289,6 +295,7 @@ def test_create_repository_with_storage_quota_calls_borg_with_storage_quota_flag
def test_create_repository_with_make_parent_dirs_calls_borg_with_make_parent_dirs_flag():
insert_repo_info_command_not_found_mock()
insert_repo_create_command_mock((*REPO_CREATE_COMMAND, '--make-parent-dirs', '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -355,6 +362,7 @@ def test_create_repository_with_log_debug_calls_borg_with_debug_flag():
def test_create_repository_with_lock_wait_calls_borg_with_lock_wait_flag():
insert_repo_info_command_not_found_mock()
insert_repo_create_command_mock((*REPO_CREATE_COMMAND, '--lock-wait', '5', '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -376,6 +384,7 @@ def test_create_repository_with_lock_wait_calls_borg_with_lock_wait_flag():
def test_create_repository_with_local_path_calls_borg_via_local_path():
insert_repo_info_command_not_found_mock()
insert_repo_create_command_mock(('borg1', *REPO_CREATE_COMMAND[1:], '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -402,6 +411,7 @@ def test_create_repository_with_exit_codes_calls_borg_using_them():
('borg', *REPO_CREATE_COMMAND[1:], '--repo', 'repo'),
borg_exit_codes=borg_exit_codes,
)
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -425,6 +435,7 @@ def test_create_repository_with_remote_path_calls_borg_with_remote_path_flag():
insert_repo_create_command_mock(
(*REPO_CREATE_COMMAND, '--remote-path', 'borg1', '--repo', 'repo'),
)
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -447,6 +458,7 @@ def test_create_repository_with_remote_path_calls_borg_with_remote_path_flag():
def test_create_repository_with_umask_calls_borg_with_umask_flag():
insert_repo_info_command_not_found_mock()
insert_repo_create_command_mock((*REPO_CREATE_COMMAND, '--umask', '077', '--repo', 'repo'))
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -470,6 +482,7 @@ def test_create_repository_calls_borg_with_extra_borg_options():
insert_repo_create_command_mock(
(*REPO_CREATE_COMMAND, '--extra', '--options', 'value with space', '--repo', 'repo'),
)
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -494,6 +507,7 @@ def test_create_repository_calls_borg_with_extra_borg_options_from_deprecated_in
insert_repo_create_command_mock(
(*REPO_CREATE_COMMAND, '--extra', '--options', 'value with space', '--repo', 'repo'),
)
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
@@ -518,6 +532,7 @@ def test_create_repository_calls_borg_with_working_directory():
(*REPO_CREATE_COMMAND, '--repo', 'repo'),
working_directory='/working/dir',
)
insert_logging_mock(logging.WARNING)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_flags').and_return(
(
+12
View File
@@ -8,6 +8,7 @@ from ..test_verbosity import insert_logging_mock
def test_make_repo_delete_command_with_feature_available_runs_borg_repo_delete():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
@@ -30,6 +31,7 @@ def test_make_repo_delete_command_with_feature_available_runs_borg_repo_delete()
def test_make_repo_delete_command_without_feature_available_runs_borg_delete():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(False)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
@@ -98,6 +100,7 @@ def test_make_repo_delete_command_includes_log_debug():
def test_make_repo_delete_command_includes_dry_run():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
@@ -124,6 +127,7 @@ def test_make_repo_delete_command_includes_dry_run():
def test_make_repo_delete_command_includes_remote_path():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
@@ -150,6 +154,7 @@ def test_make_repo_delete_command_includes_remote_path():
def test_make_repo_delete_command_includes_umask():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').replace_with(
lambda name, value: (f'--{name}', value) if value else (),
@@ -174,6 +179,7 @@ def test_make_repo_delete_command_includes_umask():
def test_make_repo_delete_command_includes_lock_wait():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
@@ -200,6 +206,7 @@ def test_make_repo_delete_command_includes_lock_wait():
def test_make_repo_delete_command_without_feature_available_includes_delete_extra_borg_options():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(False)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
@@ -222,6 +229,7 @@ def test_make_repo_delete_command_without_feature_available_includes_delete_extr
def test_make_repo_delete_command_with_feature_available_includes_delete_extra_borg_options():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
@@ -244,6 +252,7 @@ def test_make_repo_delete_command_with_feature_available_includes_delete_extra_b
def test_make_repo_delete_command_includes_list():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
@@ -270,6 +279,7 @@ def test_make_repo_delete_command_includes_list():
def test_make_repo_delete_command_includes_force():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
@@ -292,6 +302,7 @@ def test_make_repo_delete_command_includes_force():
def test_make_repo_delete_command_includes_force_twice():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
@@ -314,6 +325,7 @@ def test_make_repo_delete_command_includes_force_twice():
def test_make_repo_delete_command_with_output_file_omits_log_json():
insert_logging_mock(logging.WARNING)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
+14 -2
View File
@@ -38,6 +38,7 @@ def test_display_repository_info_calls_borg_with_flags():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.display_repository_info(
repository_path='repo',
@@ -74,6 +75,7 @@ def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.display_repository_info(
repository_path='repo',
@@ -116,6 +118,7 @@ def test_display_repository_info_with_log_info_calls_borg_with_info_flag():
borg_exit_codes=None,
)
insert_logging_mock(logging.INFO)
module.display_repository_info(
repository_path='repo',
config={},
@@ -148,8 +151,8 @@ def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_out
borg_exit_codes=None,
).and_yield('[]')
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
insert_logging_mock(logging.INFO)
json_output = module.display_repository_info(
repository_path='repo',
config={},
@@ -226,8 +229,8 @@ def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_ou
borg_exit_codes=None,
).and_yield('[]')
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
insert_logging_mock(logging.DEBUG)
json_output = module.display_repository_info(
repository_path='repo',
config={},
@@ -262,6 +265,7 @@ def test_display_repository_info_with_json_calls_borg_with_json_flag():
borg_exit_codes=None,
).and_yield('[]')
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
insert_logging_mock(logging.WARNING)
json_output = module.display_repository_info(
repository_path='repo',
@@ -305,6 +309,7 @@ def test_display_repository_info_with_local_path_calls_borg_via_local_path():
borg_local_path='borg1',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.display_repository_info(
repository_path='repo',
@@ -348,6 +353,7 @@ def test_display_repository_info_with_exit_codes_calls_borg_using_them():
borg_local_path='borg',
borg_exit_codes=borg_exit_codes,
)
insert_logging_mock(logging.WARNING)
module.display_repository_info(
repository_path='repo',
@@ -389,6 +395,7 @@ def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_fl
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.display_repository_info(
repository_path='repo',
@@ -431,6 +438,7 @@ def test_display_repository_info_with_umask_calls_borg_with_umask_flags():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.display_repository_info(
repository_path='repo',
@@ -467,6 +475,7 @@ def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_flags(
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.display_repository_info(
repository_path='repo',
@@ -509,6 +518,7 @@ def test_display_repository_info_without_feature_available_calls_borg_with_info_
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.display_repository_info(
repository_path='repo',
@@ -561,6 +571,7 @@ def test_display_repository_info_with_feature_available_calls_borg_with_repo_inf
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.display_repository_info(
repository_path='repo',
@@ -604,6 +615,7 @@ def test_display_repository_info_calls_borg_with_working_directory():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.display_repository_info(
repository_path='repo',
+30
View File
@@ -126,6 +126,7 @@ def test_get_latest_archive_calls_borg_with_flags():
environment=None,
working_directory=None,
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -218,6 +219,7 @@ def test_get_latest_archive_with_local_path_calls_borg_via_local_path():
borg_local_path='borg1',
borg_exit_codes=None,
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -250,6 +252,7 @@ def test_get_latest_archive_with_exit_codes_calls_borg_using_them():
borg_local_path='borg',
borg_exit_codes=borg_exit_codes,
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -283,6 +286,7 @@ def test_get_latest_archive_with_remote_path_calls_borg_with_remote_path_flags()
borg_local_path='borg',
borg_exit_codes=None,
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -317,6 +321,7 @@ def test_get_latest_archive_with_umask_calls_borg_with_umask_flags():
borg_local_path='borg',
borg_exit_codes=None,
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -346,6 +351,7 @@ def test_get_latest_archive_without_archives_raises():
borg_local_path='borg',
borg_exit_codes=None,
).and_yield(json.dumps({'archives': []}))
insert_logging_mock(logging.WARNING)
with pytest.raises(ValueError):
module.get_latest_archive(
@@ -377,6 +383,7 @@ def test_get_latest_archive_with_lock_wait_calls_borg_with_lock_wait_flags():
borg_local_path='borg',
borg_exit_codes=None,
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -409,6 +416,7 @@ def test_get_latest_archive_with_match_archives_calls_borg_with_match_archives_f
borg_local_path='borg',
borg_exit_codes=None,
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -447,6 +455,7 @@ def test_get_latest_archive_calls_borg_with_list_extra_borg_options():
borg_local_path='borg',
borg_exit_codes=None,
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -485,6 +494,7 @@ def test_get_latest_archive_with_feature_available_calls_borg_with_repo_list_ext
borg_local_path='borg',
borg_exit_codes=None,
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -518,6 +528,7 @@ def test_get_latest_archive_with_consider_checkpoints_calls_borg_with_consider_c
borg_local_path='borg',
borg_exit_codes=None,
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -552,6 +563,7 @@ def test_get_latest_archive_with_consider_checkpoints_and_feature_available_call
borg_local_path='borg',
borg_exit_codes=None,
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -585,6 +597,7 @@ def test_get_latest_archive_calls_borg_with_working_directory():
environment=None,
working_directory='/working/dir',
).and_yield(json.dumps({'archives': [expected_archive]}))
insert_logging_mock(logging.WARNING)
assert (
module.get_latest_archive(
@@ -758,6 +771,7 @@ def test_make_repo_list_command_includes_lock_wait():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -787,6 +801,7 @@ def test_make_repo_list_command_includes_list_extra_borg_options():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -816,6 +831,7 @@ def test_make_repo_list_command_with_feature_available_includes_repo_list_extra_
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -845,6 +861,7 @@ def test_make_repo_list_command_includes_local_path():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -877,6 +894,7 @@ def test_make_repo_list_command_includes_remote_path():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -909,6 +927,7 @@ def test_make_repo_list_command_includes_umask():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -940,6 +959,7 @@ def test_make_repo_list_command_transforms_prefix_into_match_archives():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -962,6 +982,7 @@ def test_make_repo_list_command_prefers_prefix_over_archive_name_format():
flexmock(module.flags).should_receive('make_match_archives_flags').never()
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -986,6 +1007,7 @@ def test_make_repo_list_command_transforms_archive_name_format_into_match_archiv
).and_return(('--match-archives', 'sh:bar-*'))
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -1017,6 +1039,7 @@ def test_make_repo_list_command_includes_format_from_command_line():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -1047,6 +1070,7 @@ def test_make_repo_list_command_includes_short():
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -1091,6 +1115,7 @@ def test_make_repo_list_command_includes_additional_flags(argument_name):
(f"--{argument_name.replace('_', '-')}", 'value'),
)
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -1134,6 +1159,7 @@ def test_make_repo_list_command_with_match_archives_calls_borg_with_match_archiv
).and_return(('--match-archives', 'foo-*'))
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -1161,6 +1187,7 @@ def test_list_repository_calls_borg_command():
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_yield('').once()
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
module.list_repository(
@@ -1178,6 +1205,7 @@ def test_list_repository_with_json_calls_borg_json_command_only():
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_yield('{}')
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
assert (
@@ -1204,6 +1232,7 @@ def test_make_repo_list_command_with_date_based_matching_calls_borg_with_date_ba
('--newer', '1d', '--newest', '1y', '--older', '1m', '--oldest', '1w'),
)
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.WARNING)
command = module.make_repo_list_command(
repository_path='repo',
@@ -1256,6 +1285,7 @@ def test_list_repository_calls_borg_with_working_directory():
borg_local_path=object,
borg_exit_codes=object,
).and_yield('').once()
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
module.list_repository(
+18
View File
@@ -26,6 +26,7 @@ def test_transfer_archives_calls_borg_with_flags():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -63,6 +64,7 @@ def test_transfer_archives_with_dry_run_calls_borg_with_dry_run_flag():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=True,
@@ -98,6 +100,7 @@ def test_transfer_archives_with_log_info_calls_borg_with_info_flag():
borg_exit_codes=None,
)
insert_logging_mock(logging.INFO)
module.transfer_archives(
dry_run=False,
repository_path='repo',
@@ -170,6 +173,7 @@ def test_transfer_archives_with_archive_calls_borg_with_match_archives_flag():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -208,6 +212,7 @@ def test_transfer_archives_with_match_archives_calls_borg_with_match_archives_fl
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -246,6 +251,7 @@ def test_transfer_archives_with_archive_name_format_calls_borg_with_match_archiv
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -280,6 +286,7 @@ def test_transfer_archives_with_local_path_calls_borg_via_local_path():
borg_local_path='borg2',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -316,6 +323,7 @@ def test_transfer_archives_with_exit_codes_calls_borg_using_them():
borg_local_path='borg',
borg_exit_codes=borg_exit_codes,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -354,6 +362,7 @@ def test_transfer_archives_with_remote_path_calls_borg_with_remote_path_flags():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -391,6 +400,7 @@ def test_transfer_archives_with_umask_calls_borg_with_umask_flags():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -429,6 +439,7 @@ def test_transfer_archives_with_lock_wait_calls_borg_with_lock_wait_flags():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -464,6 +475,7 @@ def test_transfer_archives_calls_borg_with_extra_borg_options():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -501,6 +513,7 @@ def test_transfer_archives_with_progress_calls_borg_with_progress_flags():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -538,6 +551,7 @@ def test_transfer_archives_with_log_json_and_progress_calls_borg_with_both_flags
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -576,6 +590,7 @@ def test_transfer_archives_passes_through_arguments_to_borg(argument_name):
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -613,6 +628,7 @@ def test_transfer_archives_with_source_repository_calls_borg_with_other_repo_fla
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -662,6 +678,7 @@ def test_transfer_archives_with_date_based_matching_calls_borg_with_date_based_f
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
@@ -701,6 +718,7 @@ def test_transfer_archives_calls_borg_with_working_directory():
borg_local_path='borg',
borg_exit_codes=None,
)
insert_logging_mock(logging.WARNING)
module.transfer_archives(
dry_run=False,
+5
View File
@@ -26,6 +26,7 @@ def insert_execute_command_mock(
def test_unmount_archive_calls_borg_with_required_parameters():
insert_execute_command_mock(('borg', 'umount', '--log-json', '/mnt'))
insert_logging_mock(logging.WARNING)
module.unmount_archive(config={}, mount_point='/mnt')
@@ -49,6 +50,7 @@ def test_unmount_archive_calls_borg_with_extra_borg_options():
('borg', 'umount', '--log-json', '--extra', 'value with space', '/mnt'),
borg_local_path='borg',
)
insert_logging_mock(logging.WARNING)
module.unmount_archive(
config={'extra_borg_options': {'umount': '--extra "value with space"'}}, mount_point='/mnt'
@@ -57,6 +59,7 @@ def test_unmount_archive_calls_borg_with_extra_borg_options():
def test_unmount_archive_calls_borg_with_local_path():
insert_execute_command_mock(('borg1', 'umount', '--log-json', '/mnt'), borg_local_path='borg1')
insert_logging_mock(logging.WARNING)
module.unmount_archive(config={}, mount_point='/mnt', local_path='borg1')
@@ -66,6 +69,7 @@ def test_unmount_archive_calls_borg_with_exit_codes():
insert_execute_command_mock(
('borg', 'umount', '--log-json', '/mnt'), borg_exit_codes=borg_exit_codes
)
insert_logging_mock(logging.WARNING)
module.unmount_archive(config={'borg_exit_codes': borg_exit_codes}, mount_point='/mnt')
@@ -74,5 +78,6 @@ def test_unmount_archive_calls_borg_with_working_directory():
insert_execute_command_mock(
('borg', 'umount', '--log-json', '/mnt'), working_directory='/working/dir'
)
insert_logging_mock(logging.WARNING)
module.unmount_archive(config={'working_directory': '/working/dir'}, mount_point='/mnt')
+5
View File
@@ -32,6 +32,7 @@ def insert_execute_command_and_capture_output_mock(
def test_local_borg_version_calls_borg_with_required_parameters():
insert_execute_command_and_capture_output_mock(('borg', '--version', '--log-json'))
insert_logging_mock(logging.WARNING)
flexmock(module.environment).should_receive('make_environment')
assert module.local_borg_version({}) == VERSION
@@ -59,6 +60,7 @@ def test_local_borg_version_with_local_borg_path_calls_borg_with_it():
insert_execute_command_and_capture_output_mock(
('borg1', '--version', '--log-json'), borg_local_path='borg1'
)
insert_logging_mock(logging.WARNING)
flexmock(module.environment).should_receive('make_environment')
assert module.local_borg_version({}, 'borg1') == VERSION
@@ -70,6 +72,7 @@ def test_local_borg_version_with_borg_exit_codes_calls_using_with_them():
('borg', '--version', '--log-json'),
borg_exit_codes=borg_exit_codes,
)
insert_logging_mock(logging.WARNING)
flexmock(module.environment).should_receive('make_environment')
assert module.local_borg_version({'borg_exit_codes': borg_exit_codes}) == VERSION
@@ -79,6 +82,7 @@ def test_local_borg_version_with_invalid_version_raises():
insert_execute_command_and_capture_output_mock(
('borg', '--version', '--log-json'), version_output='wtf'
)
insert_logging_mock(logging.WARNING)
flexmock(module.environment).should_receive('make_environment')
with pytest.raises(ValueError):
@@ -90,6 +94,7 @@ def test_local_borg_version_calls_borg_with_working_directory():
('borg', '--version', '--log-json'),
working_directory='/working/dir',
)
insert_logging_mock(logging.WARNING)
flexmock(module.environment).should_receive('make_environment')
assert module.local_borg_version({'working_directory': '/working/dir'}) == VERSION
+37
View File
@@ -2095,6 +2095,43 @@ def test_collect_highlander_action_summary_logs_error_on_run_show_failure():
assert {log.levelno for log in logs} == {logging.CRITICAL}
def test_collect_highlander_action_summary_logs_nothing_additional_for_success_with_browse():
flexmock(module.borgmatic.actions.browse.run).should_receive('run_browse')
arguments = {
'browse': flexmock(),
'global': flexmock(),
}
logs = tuple(
module.collect_highlander_action_summary_logs(
{'test.yaml': {}},
arguments=arguments,
configuration_parse_errors=False,
),
)
assert not logs
def test_collect_highlander_action_summary_logs_error_on_run_browse_failure():
flexmock(module.borgmatic.actions.browse.run).should_receive('run_browse').and_raise(
ValueError,
)
arguments = {
'browse': flexmock(),
'global': flexmock(),
}
logs = tuple(
module.collect_highlander_action_summary_logs(
{'test.yaml': {}},
arguments=arguments,
configuration_parse_errors=False,
),
)
assert {log.levelno for log in logs} == {logging.CRITICAL}
def test_collect_configuration_run_summary_logs_info_for_success():
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks').with_args(
+15 -5
View File
@@ -66,7 +66,9 @@ def test_initialize_monitor_without_send_logs_does_not_add_handler():
def test_ping_monitor_respects_dry_run():
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda url, config: url)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload',
).and_return('loggy log')
@@ -83,7 +85,9 @@ def test_ping_monitor_respects_dry_run():
def test_ping_monitor_with_no_states_does_not_notify():
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler').never()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda url, config: url)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload',
).never()
@@ -100,7 +104,9 @@ def test_ping_monitor_with_no_states_does_not_notify():
def test_ping_monitor_notifies_fail_by_default():
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda url, config: url)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload',
).and_return('')
@@ -123,7 +129,9 @@ def test_ping_monitor_notifies_fail_by_default():
def test_ping_monitor_with_logs_appends_logs_to_body():
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda url, config: url)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload',
).and_return('loggy log')
@@ -146,7 +154,9 @@ def test_ping_monitor_with_logs_appends_logs_to_body():
def test_ping_monitor_with_finish_default_config_notifies():
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda url, config: url)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload',
).and_return('')
+17
View File
@@ -779,6 +779,7 @@ def test_raise_for_process_errors_with_no_processes_bails():
module.raise_for_process_errors(
buffer_readers,
process_metadatas={},
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -795,6 +796,7 @@ def test_raise_for_process_errors_with_running_process_bails():
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -812,6 +814,7 @@ def test_raise_for_process_errors_with_running_process_and_no_buffer_readers_wai
module.raise_for_process_errors(
buffer_readers={},
process_metadatas=process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -829,6 +832,7 @@ def test_raise_for_process_errors_with_successful_process_bails():
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -846,6 +850,7 @@ def test_raise_for_process_errors_with_warning_process_returns_warning_status():
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -860,6 +865,7 @@ def test_raise_for_process_errors_with_error_process_raises():
process: module.Process_metadata(last_lines=['hi', 'there'], capture=False)
}
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command)
@@ -867,6 +873,7 @@ def test_raise_for_process_errors_with_error_process_raises():
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -890,12 +897,14 @@ def test_raise_for_process_errors_with_success_process_and_warning_process_retur
flexmock(module).should_receive('interpret_exit_code').with_args(
object, 1, object, object
).and_return(module.Exit_status.WARNING)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
flexmock(module).should_receive('command_for_process').never()
assert (
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -917,6 +926,7 @@ def test_raise_for_process_errors_with_warning_process_and_error_process_raises(
flexmock(module).should_receive('interpret_exit_code').with_args(
object, 3, object, object
).and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command)
@@ -924,6 +934,7 @@ def test_raise_for_process_errors_with_warning_process_and_error_process_raises(
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -947,6 +958,7 @@ def test_raise_for_process_errors_with_warning_process_and_running_process_kills
flexmock(module).should_receive('interpret_exit_code').with_args(
object, 1, object, object
).and_return(module.Exit_status.WARNING)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command)
@@ -954,6 +966,7 @@ def test_raise_for_process_errors_with_warning_process_and_running_process_kills
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -975,6 +988,7 @@ def test_raise_for_process_errors_with_error_process_and_running_process_kills_a
flexmock(module).should_receive('interpret_exit_code').with_args(
object, 3, object, object
).and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command)
@@ -982,6 +996,7 @@ def test_raise_for_process_errors_with_error_process_and_running_process_kills_a
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -998,6 +1013,7 @@ def test_raise_for_process_errors_with_warning_process_and_long_output_raises_wi
process: module.Process_metadata(last_lines=['hi', 'there'], capture=False)
}
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command)
@@ -1005,6 +1021,7 @@ def test_raise_for_process_errors_with_warning_process_and_long_output_raises_wi
flexmock(module, ERROR_OUTPUT_MAX_LINE_COUNT=2).raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)