Compare commits

...
15 changed files with 264 additions and 26 deletions
+7
View File
@@ -1,4 +1,6 @@
2.1.6.dev0 2.1.6.dev0
* #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 * #1300: Fix the "source_directories_must_exist" option to support source directories relative to a
"working_directory". "working_directory".
* #1301: Expand the "patterns_from" and "exclude_from" options to support paths containing tildes * #1301: Expand the "patterns_from" and "exclude_from" options to support paths containing tildes
@@ -8,6 +10,11 @@
* #1303: For the MariaDB hook, include only a subset of system data when dumping the "mysql" system * #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 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/ 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 * Update the KeePassXC credential hook to support KeePassXC's secret service integration. See the
documentation for more information: documentation for more information:
https://torsion.org/borgmatic/reference/configuration/credentials/keepassxc/ 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 certifi==2026.4.22 # via apprise, requests, -r binary_requirements.in
charset-normalizer==3.4.7 # via 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 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==4.26.0 # via borgmatic, -r binary_requirements.in
jsonschema-specifications==2025.9.1 # via jsonschema, -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 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 packaging==26.2 # via borgmatic, -r binary_requirements.in
pyyaml==6.0.3 # via apprise, -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 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 requests-oauthlib==2.0.0 # via apprise, -r binary_requirements.in
rpds-py==0.30.0 # via jsonschema, referencing, -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 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
+37
View File
@@ -1,8 +1,12 @@
import argparse
import datetime
import itertools
import json import json
import logging import logging
import os import os
import borgmatic.borg.extract import borgmatic.borg.extract
import borgmatic.borg.info
import borgmatic.borg.repo_list import borgmatic.borg.repo_list
import borgmatic.config.paths import borgmatic.config.paths
@@ -100,6 +104,39 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
Raise CalledProcessError or OSError if Borg could not be run. Raise CalledProcessError or OSError if Borg could not be run.
''' '''
config = make_bootstrap_config(bootstrap_arguments) 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( archive_name = borgmatic.borg.repo_list.resolve_archive_name(
bootstrap_arguments.repository, bootstrap_arguments.repository,
bootstrap_arguments.archive, bootstrap_arguments.archive,
+8 -1
View File
@@ -374,7 +374,9 @@ def create_archive(
if json: if json:
output_log_level = None 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 output_log_level = logging.ANSWER
else: else:
output_log_level = logging.INFO output_log_level = logging.INFO
@@ -386,6 +388,11 @@ def create_archive(
create_flags += ( create_flags += (
(('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ()) (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
+ (('--stats',) if config.get('statistics') and not json and not dry_run 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 ()) + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
+ (('--progress',) if config.get('progress') else ()) + (('--progress',) if config.get('progress') else ())
+ (('--json',) if json else ()) + (('--json',) if json else ())
+7
View File
@@ -896,6 +896,13 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
action='store_true', action='store_true',
help='Display statistics of archive', 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( create_group.add_argument(
'--list', '--list',
'--files', '--files',
+12 -1
View File
@@ -1091,6 +1091,15 @@ properties:
Corresponds to the "--stats" flag on those actions. Defaults to Corresponds to the "--stats" flag on those actions. Defaults to
false. false.
example: true 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: list_details:
type: boolean type: boolean
description: | description: |
@@ -2825,7 +2834,9 @@ properties:
properties: properties:
url: url:
type: string type: string
description: URL of this Apprise service. description: |
URL of this Apprise service. Supports the
"{credential ...}" syntax.
example: "gotify://hostname/token" example: "gotify://hostname/token"
label: label:
type: string type: string
+31 -7
View File
@@ -372,13 +372,21 @@ def log_buffer_lines(
yield log_record.getMessage() 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 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 Process_metadata instance, a requested output log level for stdout, Borg's local path, a
the given processes for error or warning exit codes. If found, vent or kill any running sequence of exit code configuration dicts, and whether to capture stderr, check the given
processes. In the case of an error exit code, raise. In the case of warning, return processes for error or warning exit codes. If found, vent or kill any running processes and
Exit_status.WARNING. Otherwise, return None. 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 result_status = None
@@ -404,6 +412,13 @@ def raise_for_process_errors(buffer_readers, process_metadatas, borg_local_path,
result_status = Exit_status.WARNING result_status = Exit_status.WARNING
continue 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 last_lines = process_metadatas[process].last_lines
# If an error occurs, include its output in the raised exception so that we don't # 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 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 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. 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(): for output_buffer, reader in buffer_readers.items():
if not reader.process: if not reader.process:
@@ -446,6 +464,7 @@ def log_remaining_buffer_lines(
borg_local_path=borg_local_path, borg_local_path=borg_local_path,
command=reader.process.args, command=reader.process.args,
), ),
last_lines=process_metadatas[reader.process].last_lines,
) )
if ( if (
@@ -494,7 +513,7 @@ def log_outputs(
for buffer in output_buffers_for_process(process, exclude_stdouts) 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: while True:
yield from log_buffer_lines( yield from log_buffer_lines(
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr
@@ -502,7 +521,12 @@ def log_outputs(
if ( if (
raise_for_process_errors( 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 == Exit_status.WARNING
): ):
+7 -1
View File
@@ -1,6 +1,7 @@
import logging import logging
import operator import operator
import borgmatic.hooks.credential.parse
import borgmatic.hooks.monitoring.logs import borgmatic.hooks.monitoring.logs
import borgmatic.hooks.monitoring.monitor 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}') logger.info(f'Pinging Apprise services: {labels_string}{dry_run_string}')
apprise_object = apprise.Apprise() 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: if dry_run:
return return
@@ -5,9 +5,9 @@ eleventyNavigation:
parent: 🚨 Monitoring parent: 🚨 Monitoring
--- ---
<span class="minilink minilink-addedin">New in version 1.8.4</span> <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 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." us today such as: Telegram, Discord, Slack, Amazon SNS, Gotify, etc."
Depending on how you installed borgmatic, it may not have come with Apprise. 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. Omit `sudo` if borgmatic is installed as a non-root user.
Once Apprise is installed, configure borgmatic to notify one or more [Apprise 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 ```yaml
apprise: apprise:
+2 -1
View File
@@ -5,13 +5,14 @@ authors = [
{ name="Dan Helfman", email="witten@torsion.org" }, { name="Dan Helfman", email="witten@torsion.org" },
] ]
description = "Simple, configuration-driven backup software for servers and workstations" description = "Simple, configuration-driven backup software for servers and workstations"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Environment :: Console", "Environment :: Console",
"Intended Audience :: System Administrators", "Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python", "Programming Language :: Python",
"Topic :: Security :: Cryptography", "Topic :: Security :: Cryptography",
"Topic :: System :: Archiving :: Backup", "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 charset-normalizer==3.4.7 # via requests, -r test_requirements.in
click>=8.1.8 click>=8.1.8
codespell==2.4.2 # via -r test_requirements.in 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 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 iniconfig==2.3.0 # via pytest, -r test_requirements.in
jsonschema==4.26.0 # via -r test_requirements.in jsonschema==4.26.0 # via -r test_requirements.in
jsonschema-specifications==2025.9.1 # via jsonschema, -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 pytest-timeout==2.4.0 # via -r test_requirements.in
pyyaml>5.0.0 pyyaml>5.0.0
referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r test_requirements.in 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 requests-oauthlib==2.0.0 # via apprise, -r test_requirements.in
rpds-py==0.30.0 # via jsonschema, referencing, -r test_requirements.in rpds-py==0.30.0 # via jsonschema, referencing, -r test_requirements.in
ruamel-yaml>0.15.0 ruamel-yaml>0.15.0
typing-extensions==4.15.0 # via -r test_requirements.in 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(): def test_read_lines_uses_system_locale_when_decoding_output():
flexmock(module.locale).should_receive('getpreferredencoding').and_return('ISO-8859-1') flexmock(module.locale).should_receive('getpreferredencoding').and_return('ISO-8859-1')
process = subprocess.Popen(['echo', b'\xc4pple'], stdout=subprocess.PIPE) process = subprocess.Popen(['echo', b'\xc4pple'], stdout=subprocess.PIPE)
+74
View File
@@ -1477,6 +1477,44 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats():
'source_directories': ['foo', 'bar'], 'source_directories': ['foo', 'bar'],
'repositories': ['repo'], 'repositories': ['repo'],
'exclude_patterns': None, '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')], patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3', 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*'], 'source_directories': ['foo*'],
'repositories': ['repo'], 'repositories': ['repo'],
'exclude_patterns': None, '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')], patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3', local_borg_version='1.2.3',
+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(): 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( flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload', 'format_buffered_logs_for_payload',
).and_return('loggy log') ).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(): 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( flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload', 'format_buffered_logs_for_payload',
).never() ).never()
@@ -100,7 +104,9 @@ def test_ping_monitor_with_no_states_does_not_notify():
def test_ping_monitor_notifies_fail_by_default(): 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( flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload', 'format_buffered_logs_for_payload',
).and_return('') ).and_return('')
@@ -123,7 +129,9 @@ def test_ping_monitor_notifies_fail_by_default():
def test_ping_monitor_with_logs_appends_logs_to_body(): 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( flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload', 'format_buffered_logs_for_payload',
).and_return('loggy log') ).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(): 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( flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload', 'format_buffered_logs_for_payload',
).and_return('') ).and_return('')
+17
View File
@@ -779,6 +779,7 @@ def test_raise_for_process_errors_with_no_processes_bails():
module.raise_for_process_errors( module.raise_for_process_errors(
buffer_readers, buffer_readers,
process_metadatas={}, process_metadatas={},
output_log_level=None,
borg_local_path=flexmock(), borg_local_path=flexmock(),
borg_exit_codes=flexmock(), borg_exit_codes=flexmock(),
) )
@@ -795,6 +796,7 @@ def test_raise_for_process_errors_with_running_process_bails():
module.raise_for_process_errors( module.raise_for_process_errors(
buffer_readers, buffer_readers,
process_metadatas, process_metadatas,
output_log_level=None,
borg_local_path=flexmock(), borg_local_path=flexmock(),
borg_exit_codes=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( module.raise_for_process_errors(
buffer_readers={}, buffer_readers={},
process_metadatas=process_metadatas, process_metadatas=process_metadatas,
output_log_level=None,
borg_local_path=flexmock(), borg_local_path=flexmock(),
borg_exit_codes=flexmock(), borg_exit_codes=flexmock(),
) )
@@ -829,6 +832,7 @@ def test_raise_for_process_errors_with_successful_process_bails():
module.raise_for_process_errors( module.raise_for_process_errors(
buffer_readers, buffer_readers,
process_metadatas, process_metadatas,
output_log_level=None,
borg_local_path=flexmock(), borg_local_path=flexmock(),
borg_exit_codes=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( module.raise_for_process_errors(
buffer_readers, buffer_readers,
process_metadatas, process_metadatas,
output_log_level=None,
borg_local_path=flexmock(), borg_local_path=flexmock(),
borg_exit_codes=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) 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('interpret_exit_code').and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock() command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command) 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( module.raise_for_process_errors(
buffer_readers, buffer_readers,
process_metadatas, process_metadatas,
output_log_level=None,
borg_local_path=flexmock(), borg_local_path=flexmock(),
borg_exit_codes=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( flexmock(module).should_receive('interpret_exit_code').with_args(
object, 1, object, object object, 1, object, object
).and_return(module.Exit_status.WARNING) ).and_return(module.Exit_status.WARNING)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
flexmock(module).should_receive('command_for_process').never() flexmock(module).should_receive('command_for_process').never()
assert ( assert (
module.raise_for_process_errors( module.raise_for_process_errors(
buffer_readers, buffer_readers,
process_metadatas, process_metadatas,
output_log_level=None,
borg_local_path=flexmock(), borg_local_path=flexmock(),
borg_exit_codes=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( flexmock(module).should_receive('interpret_exit_code').with_args(
object, 3, object, object object, 3, object, object
).and_return(module.Exit_status.ERROR) ).and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock() command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command) 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( module.raise_for_process_errors(
buffer_readers, buffer_readers,
process_metadatas, process_metadatas,
output_log_level=None,
borg_local_path=flexmock(), borg_local_path=flexmock(),
borg_exit_codes=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( flexmock(module).should_receive('interpret_exit_code').with_args(
object, 1, object, object object, 1, object, object
).and_return(module.Exit_status.WARNING) ).and_return(module.Exit_status.WARNING)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock() command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command) 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( module.raise_for_process_errors(
buffer_readers, buffer_readers,
process_metadatas, process_metadatas,
output_log_level=None,
borg_local_path=flexmock(), borg_local_path=flexmock(),
borg_exit_codes=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( flexmock(module).should_receive('interpret_exit_code').with_args(
object, 3, object, object object, 3, object, object
).and_return(module.Exit_status.ERROR) ).and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock() command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command) 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( module.raise_for_process_errors(
buffer_readers, buffer_readers,
process_metadatas, process_metadatas,
output_log_level=None,
borg_local_path=flexmock(), borg_local_path=flexmock(),
borg_exit_codes=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) 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('interpret_exit_code').and_return(module.Exit_status.ERROR)
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
command = flexmock() command = flexmock()
flexmock(module).should_receive('command_for_process').and_return(command) 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( flexmock(module, ERROR_OUTPUT_MAX_LINE_COUNT=2).raise_for_process_errors(
buffer_readers, buffer_readers,
process_metadatas, process_metadatas,
output_log_level=None,
borg_local_path=flexmock(), borg_local_path=flexmock(),
borg_exit_codes=flexmock(), borg_exit_codes=flexmock(),
) )