diff --git a/NEWS b/NEWS index 8afb3c41..bbcec3ad 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,9 @@ * #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/ + * 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/ diff --git a/binary_requirements.in b/binary_requirements.in index 985a50f8..af6f4163 100644 --- a/binary_requirements.in +++ b/binary_requirements.in @@ -1,6 +1,7 @@ . apprise attrs +binaryornot certifi charset-normalizer click @@ -14,6 +15,8 @@ pyyaml referencing requests requests-oauthlib +rich rpds-py ruamel-yaml +textual urllib3 diff --git a/binary_requirements.txt b/binary_requirements.txt index 78c1bfe0..e6567ccc 100644 --- a/binary_requirements.txt +++ b/binary_requirements.txt @@ -2,6 +2,7 @@ # uv pip compile --annotation-style line binary_requirements.in -o binary_requirements.txt apprise==1.10.0 # via -r binary_requirements.in attrs==26.1.0 # via jsonschema, referencing, -r binary_requirements.in +binaryornot==0.6.0 # via -r binary_requirements.in . # via -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 @@ -9,13 +10,23 @@ 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 +linkify-it-py==2.1.0 # via markdown-it-py markdown==3.10.2 # via apprise, -r binary_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 binary_requirements.in packaging==26.2 # via borgmatic, -r binary_requirements.in +platformdirs==4.9.6 # via textual +pygments==2.20.0 # via rich, textual pyyaml==6.0.3 # via apprise, -r binary_requirements.in referencing==0.37.0 # via jsonschema, jsonschema-specifications, -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 +rich==15.0.0 # via textual, -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 +textual==8.2.7 # via -r binary_requirements.in +typing-extensions==4.15.0 # via textual +uc-micro-py==2.0.0 # via linkify-it-py urllib3==2.7.0 # via requests, -r binary_requirements.in diff --git a/borgmatic/actions/browse/__init__.py b/borgmatic/actions/browse/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/borgmatic/actions/browse/app.py b/borgmatic/actions/browse/app.py new file mode 100644 index 00000000..30894d22 --- /dev/null +++ b/borgmatic/actions/browse/app.py @@ -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() diff --git a/borgmatic/actions/browse/archive.py b/borgmatic/actions/browse/archive.py new file mode 100644 index 00000000..d474969e --- /dev/null +++ b/borgmatic/actions/browse/archive.py @@ -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 diff --git a/borgmatic/actions/browse/archives_list.py b/borgmatic/actions/browse/archives_list.py new file mode 100644 index 00000000..9182849a --- /dev/null +++ b/borgmatic/actions/browse/archives_list.py @@ -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 diff --git a/borgmatic/actions/browse/bindings.py b/borgmatic/actions/browse/bindings.py new file mode 100644 index 00000000..b2bfc4e9 --- /dev/null +++ b/borgmatic/actions/browse/bindings.py @@ -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), +) diff --git a/borgmatic/actions/browse/carousel.py b/borgmatic/actions/browse/carousel.py new file mode 100644 index 00000000..9890dfdd --- /dev/null +++ b/borgmatic/actions/browse/carousel.py @@ -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) diff --git a/borgmatic/actions/browse/configuration_files_list.py b/borgmatic/actions/browse/configuration_files_list.py new file mode 100644 index 00000000..90e451c2 --- /dev/null +++ b/borgmatic/actions/browse/configuration_files_list.py @@ -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' diff --git a/borgmatic/actions/browse/directory_list.py b/borgmatic/actions/browse/directory_list.py new file mode 100644 index 00000000..ff331e27 --- /dev/null +++ b/borgmatic/actions/browse/directory_list.py @@ -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 diff --git a/borgmatic/actions/browse/file_preview.py b/borgmatic/actions/browse/file_preview.py new file mode 100644 index 00000000..6ea37903 --- /dev/null +++ b/borgmatic/actions/browse/file_preview.py @@ -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)) + ) diff --git a/borgmatic/actions/browse/icons.py b/borgmatic/actions/browse/icons.py new file mode 100644 index 00000000..57d400af --- /dev/null +++ b/borgmatic/actions/browse/icons.py @@ -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: 'πŸ“„', +} diff --git a/borgmatic/actions/browse/loading.py b/borgmatic/actions/browse/loading.py new file mode 100644 index 00000000..24414c89 --- /dev/null +++ b/borgmatic/actions/browse/loading.py @@ -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), + ) diff --git a/borgmatic/actions/browse/logs.py b/borgmatic/actions/browse/logs.py new file mode 100644 index 00000000..c01d55c0 --- /dev/null +++ b/borgmatic/actions/browse/logs.py @@ -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' diff --git a/borgmatic/actions/browse/repositories_list.py b/borgmatic/actions/browse/repositories_list.py new file mode 100644 index 00000000..1caebc13 --- /dev/null +++ b/borgmatic/actions/browse/repositories_list.py @@ -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' diff --git a/borgmatic/actions/browse/run.py b/borgmatic/actions/browse/run.py new file mode 100644 index 00000000..26786120 --- /dev/null +++ b/borgmatic/actions/browse/run.py @@ -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() diff --git a/borgmatic/actions/browse/workers.py b/borgmatic/actions/browse/workers.py new file mode 100644 index 00000000..338ac8c8 --- /dev/null +++ b/borgmatic/actions/browse/workers.py @@ -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) diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py index 308a6f31..45391548 100644 --- a/borgmatic/borg/list.py +++ b/borgmatic/borg/list.py @@ -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( diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 13740769..25c49f7a 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -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 @@ -2004,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( @@ -2042,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'], @@ -2114,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) ): @@ -2132,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) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 83c77829..2298b239 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -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, diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 16f43d5c..b5b16586 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1140,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 @@ -1357,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. @@ -1423,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 diff --git a/borgmatic/hooks/monitoring/apprise.py b/borgmatic/hooks/monitoring/apprise.py index a4e649d2..d9f15bf3 100644 --- a/borgmatic/hooks/monitoring/apprise.py +++ b/borgmatic/hooks/monitoring/apprise.py @@ -45,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 = { diff --git a/borgmatic/logger.py b/borgmatic/logger.py index 09414615..fad810df 100644 --- a/borgmatic/logger.py +++ b/borgmatic/logger.py @@ -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) diff --git a/docs/Dockerfile b/docs/Dockerfile index 2f2f0e40..389be793 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -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 diff --git a/docs/_includes/snippet/command-line/sample.md b/docs/_includes/snippet/command-line/sample.md index 91013a84..8250a378 100644 --- a/docs/_includes/snippet/command-line/sample.md +++ b/docs/_includes/snippet/command-line/sample.md @@ -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. diff --git a/docs/how-to/develop-on-borgmatic.md b/docs/how-to/develop-on-borgmatic.md index effa2354..9edba4af 100644 --- a/docs/how-to/develop-on-borgmatic.md +++ b/docs/how-to/develop-on-borgmatic.md @@ -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 diff --git a/docs/how-to/inspect-your-backups.md b/docs/how-to/inspect-your-backups.md index 02997c9f..797d965f 100644 --- a/docs/how-to/inspect-your-backups.md +++ b/docs/how-to/inspect-your-backups.md @@ -135,3 +135,50 @@ 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 + +New in version 2.1.6 Experimental feature borgmatic has an +experimental console UI for browsing your repositories, archives, and files. +Here's what it looks like: + +borgmatic browse screenshot + +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. For instance, 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. diff --git a/docs/reference/command-line/actions/browse.md b/docs/reference/command-line/actions/browse.md new file mode 100644 index 00000000..c9fee5d7 --- /dev/null +++ b/docs/reference/command-line/actions/browse.md @@ -0,0 +1,17 @@ +--- +title: browse +eleventyNavigation: + key: browse + parent: 🎬 Actions +--- + +Experimental feature {% 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/) diff --git a/docs/static/browse.png b/docs/static/browse.png new file mode 100644 index 00000000..96681441 Binary files /dev/null and b/docs/static/browse.png differ diff --git a/pyproject.toml b/pyproject.toml index 8f626974..77ba0475 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,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" @@ -45,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 @@ -55,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 diff --git a/scripts/release b/scripts/release index 6ffd5560..670ccdf0 100755 --- a/scripts/release +++ b/scripts/release @@ -47,7 +47,7 @@ docs_path=dist/borgmatic-docs.tar.gz 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 +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 --include-package=textual --no-deployment-flag=self-execution borgmatic/commands/borgmatic.py deactivate rm -fr binary borgmatic.build borgmatic.dist standalone_binary_path="dist/borgmatic-${version}-binary-linux-glibc-x86_64" diff --git a/test_requirements.in b/test_requirements.in index 922cfd98..dc8ed6c7 100644 --- a/test_requirements.in +++ b/test_requirements.in @@ -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 diff --git a/test_requirements.txt b/test_requirements.txt index 1f58a643..3aa2cd76 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,6 +2,7 @@ # 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 +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 @@ -12,19 +13,28 @@ 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.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 +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 diff --git a/tests/integration/actions/browse/__init__.py b/tests/integration/actions/browse/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/actions/browse/test_app.py b/tests/integration/actions/browse/test_app.py new file mode 100644 index 00000000..a1ba36e5 --- /dev/null +++ b/tests/integration/actions/browse/test_app.py @@ -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' diff --git a/tests/integration/actions/browse/test_archives_list.py b/tests/integration/actions/browse/test_archives_list.py new file mode 100644 index 00000000..d5839274 --- /dev/null +++ b/tests/integration/actions/browse/test_archives_list.py @@ -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 diff --git a/tests/integration/actions/browse/test_carousel.py b/tests/integration/actions/browse/test_carousel.py new file mode 100644 index 00000000..2dfd2190 --- /dev/null +++ b/tests/integration/actions/browse/test_carousel.py @@ -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] diff --git a/tests/integration/actions/browse/test_configuration_files_list.py b/tests/integration/actions/browse/test_configuration_files_list.py new file mode 100644 index 00000000..204cef5f --- /dev/null +++ b/tests/integration/actions/browse/test_configuration_files_list.py @@ -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' diff --git a/tests/integration/actions/browse/test_directory_list.py b/tests/integration/actions/browse/test_directory_list.py new file mode 100644 index 00000000..103d0f4f --- /dev/null +++ b/tests/integration/actions/browse/test_directory_list.py @@ -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 diff --git a/tests/integration/actions/browse/test_file_preview.py b/tests/integration/actions/browse/test_file_preview.py new file mode 100644 index 00000000..cebf1787 --- /dev/null +++ b/tests/integration/actions/browse/test_file_preview.py @@ -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') diff --git a/tests/integration/actions/browse/test_loading.py b/tests/integration/actions/browse/test_loading.py new file mode 100644 index 00000000..b7f6e12c --- /dev/null +++ b/tests/integration/actions/browse/test_loading.py @@ -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()) diff --git a/tests/integration/actions/browse/test_logs.py b/tests/integration/actions/browse/test_logs.py new file mode 100644 index 00000000..6149cca5 --- /dev/null +++ b/tests/integration/actions/browse/test_logs.py @@ -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() diff --git a/tests/integration/actions/browse/test_repositories_list.py b/tests/integration/actions/browse/test_repositories_list.py new file mode 100644 index 00000000..7786c53d --- /dev/null +++ b/tests/integration/actions/browse/test_repositories_list.py @@ -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 diff --git a/tests/integration/actions/browse/test_workers.py b/tests/integration/actions/browse/test_workers.py new file mode 100644 index 00000000..22f328b1 --- /dev/null +++ b/tests/integration/actions/browse/test_workers.py @@ -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 diff --git a/tests/integration/borg/test_rename.py b/tests/integration/borg/test_rename.py index d9923018..68571231 100644 --- a/tests/integration/borg/test_rename.py +++ b/tests/integration/borg/test_rename.py @@ -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', diff --git a/tests/unit/actions/browse/__init__.py b/tests/unit/actions/browse/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/actions/browse/test_archive.py b/tests/unit/actions/browse/test_archive.py new file mode 100644 index 00000000..44f0b46c --- /dev/null +++ b/tests/unit/actions/browse/test_archive.py @@ -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 + ) diff --git a/tests/unit/actions/browse/test_directory_list.py b/tests/unit/actions/browse/test_directory_list.py new file mode 100644 index 00000000..5767a2f4 --- /dev/null +++ b/tests/unit/actions/browse/test_directory_list.py @@ -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' diff --git a/tests/unit/actions/browse/test_logs.py b/tests/unit/actions/browse/test_logs.py new file mode 100644 index 00000000..583b81a9 --- /dev/null +++ b/tests/unit/actions/browse/test_logs.py @@ -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', + ), + ), + ) diff --git a/tests/unit/actions/browse/test_run.py b/tests/unit/actions/browse/test_run.py new file mode 100644 index 00000000..6b451273 --- /dev/null +++ b/tests/unit/actions/browse/test_run.py @@ -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()), + ) diff --git a/tests/unit/actions/browse/test_workers.py b/tests/unit/actions/browse/test_workers.py new file mode 100644 index 00000000..cf02a14f --- /dev/null +++ b/tests/unit/actions/browse/test_workers.py @@ -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', + ) diff --git a/tests/unit/borg/test_borg.py b/tests/unit/borg/test_borg.py index df298724..269b96e5 100644 --- a/tests/unit/borg/test_borg.py +++ b/tests/unit/borg/test_borg.py @@ -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', diff --git a/tests/unit/borg/test_break_lock.py b/tests/unit/borg/test_break_lock.py index 41aa0751..1b43ca5f 100644 --- a/tests/unit/borg/test_break_lock.py +++ b/tests/unit/borg/test_break_lock.py @@ -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', diff --git a/tests/unit/borg/test_change_passphrase.py b/tests/unit/borg/test_change_passphrase.py index b8fa01af..666c6be2 100644 --- a/tests/unit/borg/test_change_passphrase.py +++ b/tests/unit/borg/test_change_passphrase.py @@ -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', diff --git a/tests/unit/borg/test_check.py b/tests/unit/borg/test_check.py index d68c89e0..9e64cb1e 100644 --- a/tests/unit/borg/test_check.py +++ b/tests/unit/borg/test_check.py @@ -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', diff --git a/tests/unit/borg/test_compact.py b/tests/unit/borg/test_compact.py index a370d034..5c9b6bfe 100644 --- a/tests/unit/borg/test_compact.py +++ b/tests/unit/borg/test_compact.py @@ -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, diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index 199385ff..d9063583 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -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, @@ -1542,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, @@ -1577,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, @@ -1611,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, @@ -1649,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, @@ -1718,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, @@ -1769,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, @@ -1802,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, @@ -1836,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, @@ -1908,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, @@ -1945,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, diff --git a/tests/unit/borg/test_delete.py b/tests/unit/borg/test_delete.py index f906331f..506317ef 100644 --- a/tests/unit/borg/test_delete.py +++ b/tests/unit/borg/test_delete.py @@ -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'}, diff --git a/tests/unit/borg/test_diff.py b/tests/unit/borg/test_diff.py index 040a94ee..70d3a7db 100644 --- a/tests/unit/borg/test_diff.py +++ b/tests/unit/borg/test_diff.py @@ -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', diff --git a/tests/unit/borg/test_export_key.py b/tests/unit/borg/test_export_key.py index 9995840f..51f487f1 100644 --- a/tests/unit/borg/test_export_key.py +++ b/tests/unit/borg/test_export_key.py @@ -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', diff --git a/tests/unit/borg/test_export_tar.py b/tests/unit/borg/test_export_tar.py index 58573789..57e670fc 100644 --- a/tests/unit/borg/test_export_tar.py +++ b/tests/unit/borg/test_export_tar.py @@ -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, diff --git a/tests/unit/borg/test_extract.py b/tests/unit/borg/test_extract.py index 6983248d..29bdc9e2 100644 --- a/tests/unit/borg/test_extract.py +++ b/tests/unit/borg/test_extract.py @@ -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, diff --git a/tests/unit/borg/test_import_key.py b/tests/unit/borg/test_import_key.py index 3bd9fc58..6aed2d67 100644 --- a/tests/unit/borg/test_import_key.py +++ b/tests/unit/borg/test_import_key.py @@ -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', diff --git a/tests/unit/borg/test_info.py b/tests/unit/borg/test_info.py index eb34b6a3..2ed8a07c 100644 --- a/tests/unit/borg/test_info.py +++ b/tests/unit/borg/test_info.py @@ -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', diff --git a/tests/unit/borg/test_list.py b/tests/unit/borg/test_list.py index fbb3a3a7..67ed518e 100644 --- a/tests/unit/borg/test_list.py +++ b/tests/unit/borg/test_list.py @@ -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'), diff --git a/tests/unit/borg/test_mount.py b/tests/unit/borg/test_mount.py index 4d3fe57a..6e67ce32 100644 --- a/tests/unit/borg/test_mount.py +++ b/tests/unit/borg/test_mount.py @@ -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( diff --git a/tests/unit/borg/test_prune.py b/tests/unit/borg/test_prune.py index fc7d6d10..00e9e98f 100644 --- a/tests/unit/borg/test_prune.py +++ b/tests/unit/borg/test_prune.py @@ -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( diff --git a/tests/unit/borg/test_recreate.py b/tests/unit/borg/test_recreate.py index ac402728..36db2466 100644 --- a/tests/unit/borg/test_recreate.py +++ b/tests/unit/borg/test_recreate.py @@ -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', diff --git a/tests/unit/borg/test_repo_create.py b/tests/unit/borg/test_repo_create.py index e7222b52..92515671 100644 --- a/tests/unit/borg/test_repo_create.py +++ b/tests/unit/borg/test_repo_create.py @@ -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( ( diff --git a/tests/unit/borg/test_repo_delete.py b/tests/unit/borg/test_repo_delete.py index 6f096721..c7968ad0 100644 --- a/tests/unit/borg/test_repo_delete.py +++ b/tests/unit/borg/test_repo_delete.py @@ -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(()) diff --git a/tests/unit/borg/test_repo_info.py b/tests/unit/borg/test_repo_info.py index a85837ff..ee8b6316 100644 --- a/tests/unit/borg/test_repo_info.py +++ b/tests/unit/borg/test_repo_info.py @@ -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', diff --git a/tests/unit/borg/test_repo_list.py b/tests/unit/borg/test_repo_list.py index a99ad41b..47b21c73 100644 --- a/tests/unit/borg/test_repo_list.py +++ b/tests/unit/borg/test_repo_list.py @@ -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( diff --git a/tests/unit/borg/test_transfer.py b/tests/unit/borg/test_transfer.py index 65c53c84..6d973525 100644 --- a/tests/unit/borg/test_transfer.py +++ b/tests/unit/borg/test_transfer.py @@ -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, diff --git a/tests/unit/borg/test_umount.py b/tests/unit/borg/test_umount.py index f9a3cc0c..2da35e11 100644 --- a/tests/unit/borg/test_umount.py +++ b/tests/unit/borg/test_umount.py @@ -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') diff --git a/tests/unit/borg/test_version.py b/tests/unit/borg/test_version.py index f38d0592..09fca1ab 100644 --- a/tests/unit/borg/test_version.py +++ b/tests/unit/borg/test_version.py @@ -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 diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 73ae1a09..5d3f129a 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -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(