Compare commits

..
30 changed files with 496 additions and 317 deletions
+7 -5
View File
@@ -1,9 +1,6 @@
2.1.6.dev0
* #1267: Support multiple borgmatic instances run in parallel—as long as they're operating on
different Borg repositories. This required an internal change to the runtime directory path,
which may cause a one-time performance hit (Borg cache misses) for Borg 1.x users of database and
filesystem hooks. See the documentation for more information:
https://torsion.org/borgmatic/how-to/make-per-application-backups/#parallelism
* #1256: Fix a race condition in which borgmatic sometimes swallows Borg error output without
logging it.
* #1300: Fix the "source_directories_must_exist" option to support source directories relative to a
"working_directory".
* #1301: Expand the "patterns_from" and "exclude_from" options to support paths containing tildes
@@ -13,6 +10,11 @@
* #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/
* #1308: Update the Apprise monitoring hook's "url" option to support loading credentials with the
"{credential ...}" syntax. See the documentation for more information:
https://torsion.org/borgmatic/reference/configuration/credentials/
* #1309: Add a "--quick-stats" flag and corresponding "quick_statistics" option for showing only
abbreviated statistics for the "create" action. Borg 1.4.5+ only.
* Update the KeePassXC credential hook to support KeePassXC's secret service integration. See the
documentation for more information:
https://torsion.org/borgmatic/reference/configuration/credentials/keepassxc/
+3 -3
View File
@@ -6,7 +6,7 @@ attrs==26.1.0 # via jsonschema, referencing, -r binary_requirements.
certifi==2026.4.22 # via apprise, requests, -r binary_requirements.in
charset-normalizer==3.4.7 # via requests, -r binary_requirements.in
click==8.3.3 # via apprise, -r binary_requirements.in
idna==3.13 # via requests, -r binary_requirements.in
idna==3.14 # via requests, -r binary_requirements.in
jsonschema==4.26.0 # via borgmatic, -r binary_requirements.in
jsonschema-specifications==2025.9.1 # via jsonschema, -r binary_requirements.in
markdown==3.10.2 # via apprise, -r binary_requirements.in
@@ -14,8 +14,8 @@ oauthlib==3.3.1 # via requests-oauthlib, -r binary_requirements.in
packaging==26.2 # via borgmatic, -r binary_requirements.in
pyyaml==6.0.3 # via apprise, -r binary_requirements.in
referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r binary_requirements.in
requests==2.33.1 # via apprise, borgmatic, requests-oauthlib, -r binary_requirements.in
requests==2.34.0 # via apprise, borgmatic, requests-oauthlib, -r binary_requirements.in
requests-oauthlib==2.0.0 # via apprise, -r binary_requirements.in
rpds-py==0.30.0 # via jsonschema, referencing, -r binary_requirements.in
ruamel-yaml==0.19.1 # via borgmatic, -r binary_requirements.in
urllib3==2.6.3 # via requests, -r binary_requirements.in
urllib3==2.7.0 # via requests, -r binary_requirements.in
+2 -4
View File
@@ -788,7 +788,7 @@ def run_check(
'''
logger.info('Running consistency checks')
repository_id = borgmatic.borg.repo_info.get_repository_id(
repository_id = borgmatic.borg.check.get_repository_id(
repository['path'],
config,
local_borg_version,
@@ -844,9 +844,7 @@ def run_check(
if 'spot' in checks:
logger.info('Running spot check')
with borgmatic.config.paths.Runtime_directory(
config, repository_id
) as borgmatic_runtime_directory:
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
spot_check(
repository,
config,
+38 -12
View File
@@ -1,9 +1,12 @@
import argparse
import datetime
import itertools
import json
import logging
import os
import borgmatic.borg.extract
import borgmatic.borg.repo_info
import borgmatic.borg.info
import borgmatic.borg.repo_list
import borgmatic.config.paths
@@ -101,6 +104,39 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
Raise CalledProcessError or OSError if Borg could not be run.
'''
config = make_bootstrap_config(bootstrap_arguments)
if bootstrap_arguments.archive == 'latest':
archives_data = json.loads(
borgmatic.borg.info.display_archives_info(
bootstrap_arguments.repository,
config,
local_borg_version,
info_arguments=argparse.Namespace(archive=None, prefix=None, json=True),
global_arguments=global_arguments,
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
)
)['archives']
get_repo_archive_format = lambda archive_data: archive_data['command_line'][-1]
get_archive_start = lambda archive_data: datetime.datetime.fromisoformat(
archive_data['start']
)
latest_archives = {
repo_archive_format: max(archives_data, key=get_archive_start)['name']
for repo_archive_format, archives_data in itertools.groupby(
sorted(archives_data, key=get_repo_archive_format),
key=get_repo_archive_format,
)
if not repo_archive_format.endswith('checkpoint')
}
if len(latest_archives) > 1:
raise ValueError(
f'The repository appears to have multiple "latest" archives, each with a different archive name format: {", ".join(sorted(latest_archives.values()))}. Please select one with --archive.'
)
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
bootstrap_arguments.repository,
bootstrap_arguments.archive,
@@ -110,18 +146,8 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
)
repository_id = borgmatic.borg.repo_info.get_repository_id(
bootstrap_arguments.repository,
config,
local_borg_version,
global_arguments,
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
)
with borgmatic.config.paths.Runtime_directory(
config, repository_id
) as borgmatic_runtime_directory:
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
manifest_config_paths = load_config_paths_from_archive(
bootstrap_arguments.repository,
archive_name,
+1 -11
View File
@@ -42,18 +42,8 @@ def run_create(
logger.info(f'Creating archive{dry_run_label}')
working_directory = borgmatic.config.paths.get_working_directory(config)
repository_id = borgmatic.borg.repo_info.get_repository_id(
repository['path'],
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
with borgmatic.config.paths.Runtime_directory(
config, repository_id
) as borgmatic_runtime_directory:
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
patterns = pattern.process_patterns(
pattern.collect_patterns(config, working_directory),
config,
+1 -11
View File
@@ -536,18 +536,8 @@ def run_restore(
'''
logger.info(f'Restoring data sources from archive {restore_arguments.archive}')
working_directory = borgmatic.config.paths.get_working_directory(config)
repository_id = borgmatic.borg.repo_info.get_repository_id(
repository['path'],
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
with borgmatic.config.paths.Runtime_directory(
config, repository_id
) as borgmatic_runtime_directory:
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
patterns = borgmatic.actions.pattern.process_patterns(
borgmatic.actions.pattern.collect_patterns(config, working_directory),
config,
+33 -1
View File
@@ -1,8 +1,10 @@
import argparse
import json
import logging
import shlex
import borgmatic.config.paths
from borgmatic.borg import environment, feature, flags
from borgmatic.borg import environment, feature, flags, repo_info
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
@@ -80,6 +82,36 @@ def make_check_name_flags(checks, archive_filter_flags):
)
def get_repository_id(
repository_path,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, global
arguments, and local/remote commands to run, return the corresponding Borg repository ID.
Raise ValueError if the Borg repository ID cannot be determined.
'''
try:
return json.loads(
repo_info.display_repository_info(
repository_path,
config,
local_borg_version,
argparse.Namespace(json=True),
global_arguments,
local_path,
remote_path,
),
)['repository']['id']
except (json.JSONDecodeError, KeyError):
raise ValueError(f'Cannot determine Borg repository ID for {repository_path}')
def check_archives(
repository_path,
config,
+8 -1
View File
@@ -374,7 +374,9 @@ def create_archive(
if json:
output_log_level = None
elif config.get('list_details') or (config.get('statistics') and not dry_run):
elif config.get('list_details') or (
(config.get('statistics') or config.get('quick_statistics')) and not dry_run
):
output_log_level = logging.ANSWER
else:
output_log_level = logging.INFO
@@ -386,6 +388,11 @@ def create_archive(
create_flags += (
(('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
+ (('--stats',) if config.get('statistics') and not json and not dry_run else ())
+ (
('--quick-stats',)
if config.get('quick_statistics') and not json and not dry_run
else ()
)
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
+ (('--progress',) if config.get('progress') else ())
+ (('--json',) if json else ())
-32
View File
@@ -1,5 +1,3 @@
import argparse
import json
import logging
import shlex
@@ -83,33 +81,3 @@ def display_repository_info(
)
return None
def get_repository_id(
repository_path,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, global
arguments, and local/remote commands to run, return the corresponding Borg repository ID.
Raise ValueError if the Borg repository ID cannot be determined.
'''
try:
return json.loads(
display_repository_info(
repository_path,
config,
local_borg_version,
argparse.Namespace(json=True),
global_arguments,
local_path,
remote_path,
),
)['repository']['id']
except (json.JSONDecodeError, KeyError):
raise ValueError(f'Cannot determine Borg repository ID for {repository_path}')
+7
View File
@@ -896,6 +896,13 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
action='store_true',
help='Display statistics of archive',
)
create_group.add_argument(
'--quick-stats',
dest='quick_statistics',
default=None,
action='store_true',
help='Display statistics of archive, skipping repository-wide "All archives" and chunk index statistics [Borg 1.4.5+ only]',
)
create_group.add_argument(
'--list',
'--files',
+20 -39
View File
@@ -1,7 +1,6 @@
import contextlib
import logging
import os
import shutil
import tempfile
from enum import Enum
@@ -85,25 +84,6 @@ def replace_temporary_subdirectory_with_glob(
)
class Fixed_name_temporary_directory:
'''
A class whose instances can stand-in for tempfile.TemporaryDirectory's, except the temporary
filename path is fixed rather than randomly generated.
'''
def __init__(self, path):
'''
Given a temporary directory path, save it off for later.
'''
self.name = path
def cleanup(self):
'''
Remove the temporary directory path.
'''
shutil.rmtree(self.name, ignore_errors=True)
class Runtime_directory:
'''
A Python context manager for creating and cleaning up the borgmatic runtime directory used for
@@ -118,17 +98,16 @@ class Runtime_directory:
automatically gets cleaned up as necessary.
'''
def __init__(self, config, repository_id):
def __init__(self, config):
'''
Given a configuration dict and the Borg ID for a repository, determine the borgmatic runtime
directory, creating a secure, temporary directory within it if necessary. Defaults to
$XDG_RUNTIME_DIR/borgmatic-[repository_id]/./borgmatic or
$RUNTIME_DIRECTORY/borgmatic-[repository_id]/./borgmatic or
$TMPDIR/borgmatic-[random]/./borgmatic or $TEMP/borgmatic-[random]/./borgmatic or
/tmp/borgmatic-[random]/./borgmatic where "[random]" is a randomly generated string and
"[repository_id]" is the Borg repository ID being operated on. Both strings are intended to
avoid path collisions, and the random string helps avoid predictable temporary path attacks
in shared temporary directories.
Given a configuration dict determine the borgmatic runtime directory, creating a secure,
temporary directory within it if necessary. Defaults to $XDG_RUNTIME_DIR/./borgmatic or
$RUNTIME_DIRECTORY/./borgmatic or $TMPDIR/borgmatic-[random]/./borgmatic or
$TEMP/borgmatic-[random]/./borgmatic or /tmp/borgmatic-[random]/./borgmatic where "[random]"
is a randomly generated string intended to avoid path collisions.
If XDG_RUNTIME_DIR or RUNTIME_DIRECTORY is set and already ends in "/borgmatic", then don't
tack on a second "/borgmatic" path component.
The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./"
does not get stored in the file path within an archive. That way, the path of the runtime
@@ -146,8 +125,7 @@ class Runtime_directory:
if not runtime_directory.startswith(os.path.sep):
raise ValueError('The runtime directory must be an absolute path')
runtime_directory = os.path.join(runtime_directory, f'borgmatic-{repository_id}')
self.temporary_directory = Fixed_name_temporary_directory(runtime_directory)
self.temporary_directory = None
else:
base_directory = (
os.environ.get('TMPDIR') or os.environ.get('TEMP') or '/tmp' # noqa: S108
@@ -163,9 +141,11 @@ class Runtime_directory:
)
runtime_directory = self.temporary_directory.name
(base_path, final_directory) = os.path.split(runtime_directory.rstrip(os.path.sep))
self.runtime_path = expand_user_in_path(
os.path.join(
runtime_directory,
base_path if final_directory == 'borgmatic' else runtime_directory,
'.', # Borg 1.4+ "slashdot" hack.
'borgmatic',
),
@@ -182,13 +162,14 @@ class Runtime_directory:
def __exit__(self, exception_type, exception, traceback):
'''
Delete the temporary directory that was created as part of initialization.
Delete any temporary directory that was created as part of initialization.
'''
# The cleanup() call errors if, for instance, there's still a
# mounted filesystem within the temporary directory. There's
# nothing we can do about that here, so swallow the error.
with contextlib.suppress(OSError):
self.temporary_directory.cleanup()
if self.temporary_directory:
# The cleanup() call errors if, for instance, there's still a
# mounted filesystem within the temporary directory. There's
# nothing we can do about that here, so swallow the error.
with contextlib.suppress(OSError):
self.temporary_directory.cleanup()
def make_runtime_directory_glob(borgmatic_runtime_directory):
+12 -1
View File
@@ -1091,6 +1091,15 @@ properties:
Corresponds to the "--stats" flag on those actions. Defaults to
false.
example: true
quick_statistics:
type: boolean
description: |
Display statistics for an archive when running supported actions,
skipping the repository-wide "All archives" and chunk index
statistics to save some time. Corresponds to the "--quick-stats"
flag on those actions. Defaults to false. (This option is supported
for Borg 1.4.5+ only.)
example: true
list_details:
type: boolean
description: |
@@ -2825,7 +2834,9 @@ properties:
properties:
url:
type: string
description: URL of this Apprise service.
description: |
URL of this Apprise service. Supports the
"{credential ...}" syntax.
example: "gotify://hostname/token"
label:
type: string
+31 -7
View File
@@ -372,13 +372,21 @@ def log_buffer_lines(
yield log_record.getMessage()
def raise_for_process_errors(buffer_readers, process_metadatas, borg_local_path, borg_exit_codes):
def raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level,
borg_local_path,
borg_exit_codes,
capture_stderr=False,
):
'''
Given a dict from buffer object to Buffer_reader, a dict from subprocess.Popen() instance to
Process_metadata instance, Borg's local path, a sequence of exit code configuration dicts, check
the given processes for error or warning exit codes. If found, vent or kill any running
processes. In the case of an error exit code, raise. In the case of warning, return
Exit_status.WARNING. Otherwise, return None.
Process_metadata instance, a requested output log level for stdout, Borg's local path, a
sequence of exit code configuration dicts, and whether to capture stderr, check the given
processes for error or warning exit codes. If found, vent or kill any running processes and
drain any remaining buffer lines. In the case of an error exit code, raise. In the case of
warning, return Exit_status.WARNING. Otherwise, return None.
'''
result_status = None
@@ -404,6 +412,13 @@ def raise_for_process_errors(buffer_readers, process_metadatas, borg_local_path,
result_status = Exit_status.WARNING
continue
# Drain and log remaining buffer lines, so no output gets lost. But swallow captured output,
# since we're raising below instead of yielding captured lines.
tuple(
log_remaining_buffer_lines(
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr
)
)
last_lines = process_metadatas[process].last_lines
# If an error occurs, include its output in the raised exception so that we don't
@@ -429,6 +444,9 @@ def log_remaining_buffer_lines(
whether to capture stderr, drain and log any remaining output lines from the buffers until
they're empty. Additionally, for any log records with a log level the same as the output log
level, yield those log messages for capture.
The main difference between this function and log_buffer_lines() is that this one doesn't just
do one "turn of the crank" of logging buffer output; it completely drains any remaining output.
'''
for output_buffer, reader in buffer_readers.items():
if not reader.process:
@@ -446,6 +464,7 @@ def log_remaining_buffer_lines(
borg_local_path=borg_local_path,
command=reader.process.args,
),
last_lines=process_metadatas[reader.process].last_lines,
)
if (
@@ -494,7 +513,7 @@ def log_outputs(
for buffer in output_buffers_for_process(process, exclude_stdouts)
}
# Log output lines for each process until they all exit.
# Log output lines for each process until they all exit or one errors.
while True:
yield from log_buffer_lines(
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr
@@ -502,7 +521,12 @@ def log_outputs(
if (
raise_for_process_errors(
buffer_readers, process_metadatas, borg_local_path, borg_exit_codes
buffer_readers,
process_metadatas,
output_log_level,
borg_local_path,
borg_exit_codes,
capture_stderr,
)
== Exit_status.WARNING
):
+7 -1
View File
@@ -1,6 +1,7 @@
import logging
import operator
import borgmatic.hooks.credential.parse
import borgmatic.hooks.monitoring.logs
import borgmatic.hooks.monitoring.monitor
@@ -76,7 +77,12 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
logger.info(f'Pinging Apprise services: {labels_string}{dry_run_string}')
apprise_object = apprise.Apprise()
apprise_object.add(list(map(operator.itemgetter('url'), hook_config.get('services'))))
apprise_object.add(
[
borgmatic.hooks.credential.parse.resolve_credential(service['url'], config)
for service in hook_config.get('services')
]
)
if dry_run:
return
+10 -16
View File
@@ -58,25 +58,19 @@ choice](https://torsion.org/borgmatic/how-to/set-up-backups/#autopilot), each
entry using borgmatic's `--config` flag instead of relying on
`/etc/borgmatic.d`.
<a id="limitations"></a>
## Parallelism
## Limitations
<span class="minilink minilink-addedin">New in version 2.1.6</span> borgmatic
supports multiple borgmatic instances run on the same machine in parallel—as
long as they're operating on different Borg repositories.
borgmatic does not currently support its own parallelism—being run multiple
times on the same machine simultaneously. In particular, many of the [data
source
hooks](https://torsion.org/borgmatic/reference/configuration/data-sources/) rely
on global borgmatic runtime files which can't be shared across processes, and
therefore multiple borgmatic instances on the same machine would interfere with
each other.
However, note that a single borgmatic instance doesn't currently support running
multiple Borg instances in parallel on the same machine.
<span class="minilink minilink-addedin">Prior to version 2.1.6</span> borgmatic
did not support parallel borgmatic runs on the same machine simultaneously. In
particular, many of the [data source
hooks](https://torsion.org/borgmatic/reference/configuration/data-sources/)
relied on global borgmatic runtime files which couldn't be shared across
processes, and therefore multiple borgmatic instances on the same machine
interfered with each other.
A single borgmatic instance also doesn't currently support running multiple Borg
instances in parallel on the same machine.
<a id="archive-naming"></a>
@@ -5,9 +5,9 @@ eleventyNavigation:
parent: 🚨 Monitoring
---
<span class="minilink minilink-addedin">New in version 1.8.4</span>
[Apprise](https://github.com/caronc/apprise/wiki) is a local notification library
[Apprise](https://appriseit.com/) is a local notification library
that "allows you to send a notification to almost all of the most popular
[notification services](https://github.com/caronc/apprise/wiki) available to
[notification services](https://appriseit.com/services/) available to
us today such as: Telegram, Discord, Slack, Amazon SNS, Gotify, etc."
Depending on how you installed borgmatic, it may not have come with Apprise.
@@ -22,7 +22,7 @@ sudo uv tool install borgmatic[Apprise]
Omit `sudo` if borgmatic is installed as a non-root user.
Once Apprise is installed, configure borgmatic to notify one or more [Apprise
services](https://github.com/caronc/apprise/wiki). For example:
services](https://appriseit.com/services/). For example:
```yaml
apprise:
@@ -29,18 +29,11 @@ probes the following values:
You can see the runtime directory path that borgmatic selects by running with
`--verbosity 2` and looking for `Using runtime directory` in the output.
Regardless of the runtime directory selected, borgmatic stores its files within
a `borgmatic` subdirectory of the runtime directory. Additionally, in the case
of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a randomly
named subdirectory in an effort to reduce path collisions and predictable
temporary path attacks in shared temporary directories.
<span class="minilink minilink-addedin">New in version 2.1.6</span> When
constructing the runtime directory, borgmatic now creates a subdirectory named
after the Borg ID of the current repository. This means that borgmatic supports
multiple borgmatic instances run [in
parallel](https://torsion.org/borgmatic/how-to/make-per-application-backups/#parallelism)—as
long as they're operating on different Borg repositories.
Regardless of the runtime directory selected, borgmatic stores its files
within a `borgmatic` subdirectory of the runtime directory. Additionally, in
the case of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a
randomly named subdirectory in an effort to reduce path collisions in shared
system temporary directories.
<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
borgmatic created temporary streaming database dumps within the `~/.borgmatic`
+2 -1
View File
@@ -5,13 +5,14 @@ authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
description = "Simple, configuration-driven backup software for servers and workstations"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
readme = "README.md"
requires-python = ">=3.9"
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python",
"Topic :: Security :: Cryptography",
"Topic :: System :: Archiving :: Backup",
+4 -4
View File
@@ -6,9 +6,9 @@ certifi==2026.4.22 # via apprise, requests, -r test_requirements.in
charset-normalizer==3.4.7 # via requests, -r test_requirements.in
click>=8.1.8
codespell==2.4.2 # via -r test_requirements.in
coverage==7.13.5 # via pytest-cov, -r test_requirements.in
coverage==7.14.0 # via pytest-cov, -r test_requirements.in
flexmock==0.13.0 # via -r test_requirements.in
idna==3.13 # via requests, -r test_requirements.in
idna==3.14 # 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
@@ -22,9 +22,9 @@ pytest-cov==7.1.0 # via -r test_requirements.in
pytest-timeout==2.4.0 # via -r test_requirements.in
pyyaml>5.0.0
referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r test_requirements.in
requests==2.33.1 # via apprise, requests-oauthlib, -r test_requirements.in
requests==2.34.0 # via apprise, requests-oauthlib, -r test_requirements.in
requests-oauthlib==2.0.0 # via apprise, -r test_requirements.in
rpds-py==0.30.0 # via jsonschema, referencing, -r test_requirements.in
ruamel-yaml>0.15.0
typing-extensions==4.15.0 # via -r test_requirements.in
urllib3==2.6.3 # via requests, -r test_requirements.in
urllib3==2.7.0 # via requests, -r test_requirements.in
+37
View File
@@ -557,6 +557,43 @@ def test_log_outputs_with_unfinished_process_re_polls():
)
def test_log_outputs_includes_error_output_when_output_spans_multiple_chunks():
flexmock(module.logger).should_receive('log')
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('command_for_process').and_return('python')
process = subprocess.Popen(
[
sys.executable,
'-c',
(
'import os, sys; '
f'os.write(sys.stdout.fileno(), b"x" * {module.READ_CHUNK_SIZE + 10}); '
'os.write(sys.stdout.fileno(), b"\\nERROR: critical failure"); '
'os.close(1); '
'os._exit(2)'
),
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
flexmock(module).should_receive('output_buffers_for_process').and_return((process.stdout,))
with pytest.raises(subprocess.CalledProcessError) as error:
tuple(
module.log_outputs(
(process,),
exclude_stdouts=(),
output_log_level=logging.INFO,
borg_local_path='borg',
borg_exit_codes=None,
)
)
assert error.value.output
assert 'ERROR: critical failure' in error.value.output
def test_read_lines_uses_system_locale_when_decoding_output():
flexmock(module.locale).should_receive('getpreferredencoding').and_return('ISO-8859-1')
process = subprocess.Popen(['echo', b'\xc4pple'], stdout=subprocess.PIPE)
@@ -171,7 +171,6 @@ def test_run_bootstrap_does_not_raise():
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
'archive',
)
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('repo')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -223,7 +222,6 @@ def test_run_bootstrap_translates_ssh_command_argument_to_config():
local_path='borg7',
remote_path='borg8',
).and_return('archive')
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('repo')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
+4 -4
View File
@@ -1838,7 +1838,7 @@ def test_spot_check_without_any_source_paths_errors():
def test_run_check_checks_archives_for_configured_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
@@ -1873,7 +1873,7 @@ def test_run_check_checks_archives_for_configured_repository():
def test_run_check_runs_configured_extract_check():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
@@ -1906,7 +1906,7 @@ def test_run_check_runs_configured_extract_check():
def test_run_check_runs_configured_spot_check():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
@@ -1942,7 +1942,7 @@ def test_run_check_runs_configured_spot_check():
def test_run_check_without_checks_runs_nothing_except_hooks():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
-7
View File
@@ -11,7 +11,6 @@ from borgmatic.actions import create as module
def test_run_create_executes_and_calls_hooks_for_configured_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -50,7 +49,6 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
def test_run_create_with_both_list_and_json_errors():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').never()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
@@ -82,7 +80,6 @@ def test_run_create_with_both_list_and_json_errors():
def test_run_create_with_both_list_and_progress_errors():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').never()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
@@ -114,7 +111,6 @@ def test_run_create_with_both_list_and_progress_errors():
def test_run_create_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -160,7 +156,6 @@ def test_run_create_with_active_dumps_roundtrips_via_checkpoint_archive():
mock_dump_process.should_receive('poll').and_return(None).and_return(0)
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -239,7 +234,6 @@ def test_run_create_with_active_dumps_json_updates_archive_info():
}
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -322,7 +316,6 @@ def mock_dump_cleanup(config, borgmatic_runtime_directory, patterns, dry_run):
def test_run_create_with_active_dumps_removes_data_source_dumps_with_original_patterns():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
-5
View File
@@ -1175,7 +1175,6 @@ def test_run_restore_restores_each_data_source():
}
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
)
@@ -1250,7 +1249,6 @@ def test_run_restore_restores_data_source_by_falling_back_to_all_name():
}
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
)
@@ -1313,7 +1311,6 @@ def test_run_restore_restores_data_source_configured_with_all_name():
}
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
)
@@ -1398,7 +1395,6 @@ def test_run_restore_skips_missing_data_source():
}
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
)
@@ -1483,7 +1479,6 @@ def test_run_restore_restores_data_sources_from_different_hooks():
}
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
)
+48
View File
@@ -272,6 +272,54 @@ def test_make_check_name_flags_with_extract_omits_extract_flag():
assert flags == ()
def test_get_repository_id_with_valid_json_does_not_raise():
config = {}
flexmock(module.repo_info).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}',
)
assert module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_get_repository_id_with_json_error_raises():
config = {}
flexmock(module.repo_info).should_receive('display_repository_info').and_return(
'{"unexpected": {"id": "repo"}}',
)
with pytest.raises(ValueError):
module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_get_repository_id_with_missing_json_keys_raises():
config = {}
flexmock(module.repo_info).should_receive('display_repository_info').and_return('{invalid JSON')
with pytest.raises(ValueError):
module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_check_archives_with_progress_passes_through_to_borg():
config = {'progress': True}
flexmock(module).should_receive('make_check_name_flags').with_args(
+74
View File
@@ -1477,6 +1477,44 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats():
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
'statistics': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
global_arguments=flexmock(),
borgmatic_runtime_directory='/borgmatic/run',
)
def test_create_archive_with_quick_stats_and_dry_run_calls_borg_without_quick_stats():
# --dry-run and --quick-stats are mutually exclusive, see:
# https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_base_create_command').and_return(
(('borg', 'create', '--dry-run'), REPO_ARCHIVE, flexmock()),
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--dry-run', '--info', *REPO_ARCHIVE),
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
environment=None,
)
insert_logging_mock(logging.INFO)
module.create_archive(
dry_run=True,
repository_path='repo',
config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
'quick_statistics': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
@@ -1806,6 +1844,42 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
'source_directories': ['foo*'],
'repositories': ['repo'],
'exclude_patterns': None,
'statistics': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
global_arguments=flexmock(),
borgmatic_runtime_directory='/borgmatic/run',
json=True,
)
assert json_output == '[]'
def test_create_archive_with_quick_stats_and_json_calls_borg_without_quick_stats_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_base_create_command').and_return(
(('borg', 'create'), REPO_ARCHIVE, flexmock()),
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'create', '--json', *REPO_ARCHIVE),
working_directory=None,
environment=None,
borg_local_path='borg',
borg_exit_codes=None,
).and_yield('[]')
json_output = module.create_archive(
dry_run=False,
repository_path='repo',
config={
'source_directories': ['foo*'],
'repositories': ['repo'],
'exclude_patterns': None,
'quick_statistics': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
-49
View File
@@ -1,6 +1,5 @@
import logging
import pytest
from flexmock import flexmock
from borgmatic.borg import repo_info as module
@@ -613,51 +612,3 @@ def test_display_repository_info_calls_borg_with_working_directory():
repo_info_arguments=flexmock(json=False),
global_arguments=flexmock(),
)
def test_get_repository_id_with_valid_json_does_not_raise():
config = {}
flexmock(module).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}',
)
assert module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_get_repository_id_with_json_error_raises():
config = {}
flexmock(module).should_receive('display_repository_info').and_return(
'{"unexpected": {"id": "repo"}}',
)
with pytest.raises(ValueError):
module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_get_repository_id_with_missing_json_keys_raises():
config = {}
flexmock(module).should_receive('display_repository_info').and_return('{invalid JSON')
with pytest.raises(ValueError):
module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
+107 -81
View File
@@ -62,58 +62,65 @@ def test_replace_temporary_subdirectory_with_glob_uses_custom_temporary_director
)
def test_fixed_name_temporary_directory_cleanup_does_not_raise():
flexmock(module.shutil).should_receive('rmtree')
module.Fixed_name_temporary_directory('/path').cleanup()
def test_runtime_directory_uses_config_option():
flexmock(module).should_receive('Fixed_name_temporary_directory').and_return(
flexmock(cleanup=lambda: None)
)
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os).should_receive('makedirs')
config = {'user_runtime_directory': '/run', 'borgmatic_source_directory': '/nope'}
with module.Runtime_directory(config, repository_id='id') as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/borgmatic-id/./borgmatic'
with module.Runtime_directory(config) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
def test_runtime_directory_uses_config_option_without_adding_duplicate_borgmatic_subdirectory():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os).should_receive('makedirs')
config = {'user_runtime_directory': '/run/borgmatic', 'borgmatic_source_directory': '/nope'}
with module.Runtime_directory(config) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
def test_runtime_directory_with_relative_config_option_errors():
flexmock(module.os).should_receive('makedirs').never()
config = {'user_runtime_directory': 'run', 'borgmatic_source_directory': '/nope'}
with pytest.raises(ValueError), module.Runtime_directory(config, repository_id='id'):
with pytest.raises(ValueError), module.Runtime_directory(config):
pass
def test_runtime_directory_falls_back_to_xdg_runtime_dir(monkeypatch):
flexmock(module).should_receive('Fixed_name_temporary_directory').and_return(
flexmock(cleanup=lambda: None)
)
def test_runtime_directory_falls_back_to_xdg_runtime_dir():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
monkeypatch.setenv('XDG_RUNTIME_DIR', '/run')
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(
'/run',
)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/borgmatic-id/./borgmatic'
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
def test_runtime_directory_with_relative_xdg_runtime_dir_errors(monkeypatch):
monkeypatch.setenv('XDG_RUNTIME_DIR', 'run')
def test_runtime_directory_falls_back_to_xdg_runtime_dir_without_adding_duplicate_borgmatic_subdirectory():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(
'/run/borgmatic',
)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
def test_runtime_directory_with_relative_xdg_runtime_dir_errors():
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return('run')
flexmock(module.os).should_receive('makedirs').never()
with pytest.raises(ValueError), module.Runtime_directory({}, repository_id='id'):
with pytest.raises(ValueError), module.Runtime_directory({}):
pass
def test_runtime_directory_falls_back_to_runtime_directory(monkeypatch):
flexmock(module).should_receive('Fixed_name_temporary_directory').and_return(
flexmock(cleanup=lambda: None)
)
def test_runtime_directory_falls_back_to_runtime_directory():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
@@ -121,12 +128,26 @@ def test_runtime_directory_falls_back_to_runtime_directory(monkeypatch):
)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/borgmatic-id/./borgmatic'
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
def test_runtime_directory_with_relative_runtime_directory_errors(monkeypatch):
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
def test_runtime_directory_falls_back_to_runtime_directory_without_adding_duplicate_borgmatic_subdirectory():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
'/run/borgmatic',
)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
def test_runtime_directory_with_relative_runtime_directory_errors():
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
@@ -134,21 +155,19 @@ def test_runtime_directory_with_relative_runtime_directory_errors(monkeypatch):
)
flexmock(module.os).should_receive('makedirs').never()
with pytest.raises(ValueError), module.Runtime_directory({}, repository_id='id'):
with pytest.raises(ValueError), module.Runtime_directory({}):
pass
def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_that_get_cleaned_up(
monkeypatch,
):
def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_that_get_cleaned_up():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
monkeypatch.setenv('TMPDIR', '/run')
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('/run')
temporary_directory = flexmock(name='/run/borgmatic-1234')
temporary_directory.should_receive('cleanup').once()
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
@@ -157,37 +176,35 @@ def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_
).and_return(temporary_directory)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/borgmatic-1234/./borgmatic'
def test_runtime_directory_with_relative_tmpdir_errors(monkeypatch):
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
def test_runtime_directory_with_relative_tmpdir_errors():
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
monkeypatch.setenv('TMPDIR', 'run')
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('run')
flexmock(module.tempfile).should_receive('TemporaryDirectory').never()
flexmock(module.os).should_receive('makedirs').never()
with pytest.raises(ValueError), module.Runtime_directory({}, repository_id='id'):
with pytest.raises(ValueError), module.Runtime_directory({}):
pass
def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_that_get_cleaned_up(
monkeypatch,
):
def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_that_get_cleaned_up():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
monkeypatch.delenv('TMPDIR', raising=False)
monkeypatch.setenv('TEMP', '/run')
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return('/run')
temporary_directory = flexmock(name='/run/borgmatic-1234')
temporary_directory.should_receive('cleanup').once()
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
@@ -196,38 +213,36 @@ def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_th
).and_return(temporary_directory)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/borgmatic-1234/./borgmatic'
def test_runtime_directory_with_relative_temp_errors(monkeypatch):
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
def test_runtime_directory_with_relative_temp_errors():
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
monkeypatch.delenv('TMPDIR', raising=False)
monkeypatch.setenv('TEMP', 'run')
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return('run')
flexmock(module.tempfile).should_receive('TemporaryDirectory').never()
flexmock(module.os).should_receive('makedirs')
with pytest.raises(ValueError), module.Runtime_directory({}, repository_id='id'):
with pytest.raises(ValueError), module.Runtime_directory({}):
pass
def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_subdirectory_that_get_cleaned_up(
monkeypatch,
):
def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_subdirectory_that_get_cleaned_up():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
monkeypatch.delenv('TMPDIR', raising=False)
monkeypatch.delenv('TEMP', raising=False)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return(None)
temporary_directory = flexmock(name='/tmp/borgmatic-1234')
temporary_directory.should_receive('cleanup').once()
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
@@ -236,20 +251,20 @@ def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_
).and_return(temporary_directory)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/tmp/borgmatic-1234/./borgmatic'
def test_runtime_directory_with_erroring_cleanup_does_not_raise(monkeypatch):
def test_runtime_directory_with_erroring_cleanup_does_not_raise():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
monkeypatch.delenv('TMPDIR', raising=False)
monkeypatch.delenv('TEMP', raising=False)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return(None)
temporary_directory = flexmock(name='/tmp/borgmatic-1234')
temporary_directory.should_receive('cleanup').and_raise(OSError).once()
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
@@ -258,7 +273,7 @@ def test_runtime_directory_with_erroring_cleanup_does_not_raise(monkeypatch):
).and_return(temporary_directory)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/tmp/borgmatic-1234/./borgmatic'
@@ -276,6 +291,7 @@ def test_make_runtime_directory_glob(borgmatic_runtime_directory, expected_glob)
def test_get_borgmatic_state_directory_uses_config_option():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').never()
assert (
module.get_borgmatic_state_directory(
@@ -285,16 +301,16 @@ def test_get_borgmatic_state_directory_uses_config_option():
)
def test_get_borgmatic_state_directory_falls_back_to_xdg_state_home(monkeypatch):
def test_get_borgmatic_state_directory_falls_back_to_xdg_state_home():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
monkeypatch.setenv('XDG_STATE_HOME', '/tmp')
flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return('/tmp')
assert module.get_borgmatic_state_directory({}) == '/tmp/borgmatic'
def test_get_borgmatic_state_directory_falls_back_to_state_directory(monkeypatch):
def test_get_borgmatic_state_directory_falls_back_to_state_directory():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
monkeypatch.delenv('XDG_STATE_HOME', raising=False)
flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return(None)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.STATE_DIRECTORY
).and_return(
@@ -304,26 +320,32 @@ def test_get_borgmatic_state_directory_falls_back_to_state_directory(monkeypatch
assert module.get_borgmatic_state_directory({}) == '/tmp/borgmatic'
def test_get_borgmatic_state_directory_defaults_to_hard_coded_path(monkeypatch):
def test_get_borgmatic_state_directory_defaults_to_hard_coded_path():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
monkeypatch.delenv('XDG_STATE_HOME', raising=False)
flexmock(module.os.environ).should_receive('get').and_return(None)
assert module.get_borgmatic_state_directory({}) == '~/.local/state/borgmatic'
def test_resolve_systemd_directory_none(monkeypatch):
monkeypatch.delenv('RUNTIME_DIRECTORY', raising=False)
monkeypatch.delenv('STATE_DIRECTORY', raising=False)
def test_resolve_systemd_directory_none():
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
None
)
flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(None)
assert module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY) is None
assert module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY) is None
def test_resolve_systemd_directory_single(monkeypatch):
def test_resolve_systemd_directory_single():
runtime_dir = '/run/borgmatic'
state_dir = '/var/lib/borgmatic'
monkeypatch.setenv('RUNTIME_DIRECTORY', runtime_dir)
monkeypatch.setenv('STATE_DIRECTORY', state_dir)
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
runtime_dir
)
flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
state_dir
)
assert (
module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY)
@@ -332,12 +354,16 @@ def test_resolve_systemd_directory_single(monkeypatch):
assert module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY) == state_dir
def test_resolve_systemd_directory_multiple(monkeypatch):
def test_resolve_systemd_directory_multiple():
runtime_dirs = '/run/borgmatic:/run/second:/run/third'
state_dirs = '/var/lib/borgmatic:/var/lib/second:/var/lib/third'
monkeypatch.setenv('RUNTIME_DIRECTORY', runtime_dirs)
monkeypatch.setenv('STATE_DIRECTORY', state_dirs)
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
runtime_dirs
)
flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
state_dirs
)
assert (
module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY)
+15 -5
View File
@@ -66,7 +66,9 @@ def test_initialize_monitor_without_send_logs_does_not_add_handler():
def test_ping_monitor_respects_dry_run():
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda url, config: url)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload',
).and_return('loggy log')
@@ -83,7 +85,9 @@ def test_ping_monitor_respects_dry_run():
def test_ping_monitor_with_no_states_does_not_notify():
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler').never()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda url, config: url)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload',
).never()
@@ -100,7 +104,9 @@ def test_ping_monitor_with_no_states_does_not_notify():
def test_ping_monitor_notifies_fail_by_default():
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda url, config: url)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload',
).and_return('')
@@ -123,7 +129,9 @@ def test_ping_monitor_notifies_fail_by_default():
def test_ping_monitor_with_logs_appends_logs_to_body():
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda url, config: url)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload',
).and_return('loggy log')
@@ -146,7 +154,9 @@ def test_ping_monitor_with_logs_appends_logs_to_body():
def test_ping_monitor_with_finish_default_config_notifies():
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda url, config: url)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload',
).and_return('')
+17
View File
@@ -779,6 +779,7 @@ def test_raise_for_process_errors_with_no_processes_bails():
module.raise_for_process_errors(
buffer_readers,
process_metadatas={},
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -795,6 +796,7 @@ def test_raise_for_process_errors_with_running_process_bails():
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -812,6 +814,7 @@ def test_raise_for_process_errors_with_running_process_and_no_buffer_readers_wai
module.raise_for_process_errors(
buffer_readers={},
process_metadatas=process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -829,6 +832,7 @@ def test_raise_for_process_errors_with_successful_process_bails():
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -846,6 +850,7 @@ def test_raise_for_process_errors_with_warning_process_returns_warning_status():
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -860,6 +865,7 @@ def test_raise_for_process_errors_with_error_process_raises():
process: module.Process_metadata(last_lines=['hi', 'there'], capture=False)
}
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command)
@@ -867,6 +873,7 @@ def test_raise_for_process_errors_with_error_process_raises():
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -890,12 +897,14 @@ def test_raise_for_process_errors_with_success_process_and_warning_process_retur
flexmock(module).should_receive('interpret_exit_code').with_args(
object, 1, object, object
).and_return(module.Exit_status.WARNING)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
flexmock(module).should_receive('command_for_process').never()
assert (
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -917,6 +926,7 @@ def test_raise_for_process_errors_with_warning_process_and_error_process_raises(
flexmock(module).should_receive('interpret_exit_code').with_args(
object, 3, object, object
).and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command)
@@ -924,6 +934,7 @@ def test_raise_for_process_errors_with_warning_process_and_error_process_raises(
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -947,6 +958,7 @@ def test_raise_for_process_errors_with_warning_process_and_running_process_kills
flexmock(module).should_receive('interpret_exit_code').with_args(
object, 1, object, object
).and_return(module.Exit_status.WARNING)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command)
@@ -954,6 +966,7 @@ def test_raise_for_process_errors_with_warning_process_and_running_process_kills
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -975,6 +988,7 @@ def test_raise_for_process_errors_with_error_process_and_running_process_kills_a
flexmock(module).should_receive('interpret_exit_code').with_args(
object, 3, object, object
).and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command)
@@ -982,6 +996,7 @@ def test_raise_for_process_errors_with_error_process_and_running_process_kills_a
module.raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)
@@ -998,6 +1013,7 @@ def test_raise_for_process_errors_with_warning_process_and_long_output_raises_wi
process: module.Process_metadata(last_lines=['hi', 'there'], capture=False)
}
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command)
@@ -1005,6 +1021,7 @@ def test_raise_for_process_errors_with_warning_process_and_long_output_raises_wi
flexmock(module, ERROR_OUTPUT_MAX_LINE_COUNT=2).raise_for_process_errors(
buffer_readers,
process_metadatas,
output_log_level=None,
borg_local_path=flexmock(),
borg_exit_codes=flexmock(),
)