Files
borgmatic/tests/unit/actions/test_create.py
T
Tobias SchlatterandDan Helfman 123f8958f6 Create a checkpoint archive first with streaming processes (#1032).
This ensures that if a streaming process fails, we do not create a
real (i.e. non-checkpoint archive), (fixes #1032).

Without this, there is a race condition where borg already creates an
archive before borgmatic can kill it.

Tested locally (with `pipx install --editable .`):

```sh
borgmatic --config config.yaml create
borgmatic --config config.yaml create --json
```

## Hold up!

Thanks for your contribution. Unfortunately, we don't use GitHub pull requests to manage code contributions to this repository (and GitHub doesn't have any way to disable pull requests entirely). Instead, please see:

https://torsion.org/borgmatic/#contributing

... which provides full instructions on how to submit pull requests. You can even use your GitHub account to login.

Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1102
Reviewed-by: Dan Helfman <witten@torsion.org>
Co-authored-by: Tobias Schlatter <schlatter.tobias@gmail.com>
Co-committed-by: Tobias Schlatter <schlatter.tobias@gmail.com>
2025-06-25 16:24:07 +00:00

419 lines
15 KiB
Python

import json
import os
import pytest
from flexmock import flexmock
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.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock()
)
flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
flexmock(module.borgmatic.hooks.dispatch).should_receive(
'call_hooks_even_if_unconfigured'
).and_return({})
flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(())
flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([])
flexmock(os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
create_arguments = flexmock(
repository=None,
progress=flexmock(),
statistics=flexmock(),
json=False,
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
list(
module.run_create(
config_filename='test.yaml',
repository={'path': 'repo'},
config={},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
)
def test_run_create_runs_with_selected_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match'
).once().and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock()
)
flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
flexmock(module.borgmatic.hooks.dispatch).should_receive(
'call_hooks_even_if_unconfigured'
).and_return({})
flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(())
flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([])
flexmock(os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
statistics=flexmock(),
json=False,
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
list(
module.run_create(
config_filename='test.yaml',
repository={'path': 'repo'},
config={},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
)
def test_run_create_bails_if_repository_does_not_match():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match'
).once().and_return(False)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
statistics=flexmock(),
json=False,
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
list(
module.run_create(
config_filename='test.yaml',
repository='repo',
config={},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
)
def test_run_create_with_both_list_and_json_errors():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match'
).once().and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
statistics=flexmock(),
json=True,
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
with pytest.raises(ValueError):
list(
module.run_create(
config_filename='test.yaml',
repository={'path': 'repo'},
config={'list_details': True},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
)
def test_run_create_with_both_list_and_progress_errors():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match'
).once().and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
statistics=flexmock(),
json=False,
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
with pytest.raises(ValueError):
list(
module.run_create(
config_filename='test.yaml',
repository={'path': 'repo'},
config={'list_details': True, 'progress': True},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
)
def test_run_create_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match'
).once().and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock()
)
flexmock(module.borgmatic.borg.create).should_receive('create_archive').once().and_return(
flexmock()
)
parsed_json = flexmock()
flexmock(module.borgmatic.actions.json).should_receive('parse_json').and_return(parsed_json)
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
flexmock(module.borgmatic.hooks.dispatch).should_receive(
'call_hooks_even_if_unconfigured'
).and_return({})
flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(())
flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([])
flexmock(os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
statistics=flexmock(),
json=True,
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
assert list(
module.run_create(
config_filename='test.yaml',
repository={'path': 'repo'},
config={},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
) == [parsed_json]
def test_run_create_with_active_dumps_roundtrips_via_checkpoint_archive():
mock_dump_process = flexmock()
mock_dump_process.should_receive('poll').and_return(None).and_return(0)
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock()
)
flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
{'dump': mock_dump_process}
)
flexmock(module.borgmatic.hooks.dispatch).should_receive(
'call_hooks_even_if_unconfigured'
).and_return({})
flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(())
flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([])
flexmock(os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
flexmock(module.borgmatic.borg.repo_list).should_receive('get_latest_archive').and_return(
{'id': 'id1', 'name': 'archive.checkpoint'}
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
flexmock(module).should_receive('rename_checkpoint_archive').with_args(
repository_path='repo',
global_arguments=global_arguments,
config={},
local_borg_version=None,
local_path=None,
remote_path=None,
).once()
create_arguments = flexmock(
repository=None,
progress=flexmock(),
statistics=flexmock(),
json=False,
list_details=flexmock(),
)
list(
module.run_create(
config_filename='test.yaml',
repository={'path': 'repo'},
config={},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
)
def test_run_create_with_active_dumps_json_updates_archive_info():
mock_dump_process = flexmock()
mock_dump_process.should_receive('poll').and_return(None).and_return(0)
borg_create_result = {
'archive': {
'command_line': ['foo'],
'name': 'archive.checkpoint',
'id': 'id1',
},
'cache': {},
'repository': {
'id': 'repo-id',
},
}
expected_create_result = {
'archive': {
'command_line': ['foo'],
'name': 'archive',
'id': 'id2',
},
'cache': {},
'repository': {'id': 'repo-id', 'label': ''},
}
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock()
)
flexmock(module.borgmatic.borg.create).should_receive('create_archive').and_return(
json.dumps(borg_create_result)
).once()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
{'dump': mock_dump_process}
)
flexmock(module.borgmatic.hooks.dispatch).should_receive(
'call_hooks_even_if_unconfigured'
).and_return({})
flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(())
flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([])
flexmock(os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
flexmock(module).should_receive('rename_checkpoint_archive').with_args(
repository_path='repo',
global_arguments=global_arguments,
config={},
local_borg_version=None,
local_path=None,
remote_path=None,
).once()
flexmock(module.borgmatic.borg.repo_list).should_receive('get_latest_archive').and_return(
{'id': 'id2', 'name': 'archive'},
)
create_arguments = flexmock(
repository=None,
progress=flexmock(),
statistics=flexmock(),
json=True,
list_details=flexmock(),
)
assert list(
module.run_create(
config_filename='test.yaml',
repository={'path': 'repo'},
config={},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
) == [expected_create_result]
def test_rename_checkpoint_archive_renames_archive():
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
flexmock(module.borgmatic.borg.repo_list).should_receive('get_latest_archive').and_return(
{'id': 'id1', 'name': 'archive.checkpoint'}
)
flexmock(module.borgmatic.borg.rename).should_receive('rename_archive').with_args(
repository_name='path',
old_archive_name='archive.checkpoint',
new_archive_name='archive',
dry_run=False,
config={},
local_borg_version=None,
local_path=None,
remote_path=None,
)
module.rename_checkpoint_archive(
repository_path='path',
global_arguments=global_arguments,
config={},
local_borg_version=None,
local_path=None,
remote_path=None,
)
def test_rename_checkpoint_archive_checks_suffix():
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
flexmock(module.borgmatic.borg.repo_list).should_receive('get_latest_archive').and_return(
{'id': 'id1', 'name': 'unexpected-archive'}
)
with pytest.raises(
ValueError,
match='Latest archive did not have a .checkpoint suffix. Got: unexpected-archive',
):
module.rename_checkpoint_archive(
repository_path='path',
global_arguments=global_arguments,
config={},
local_borg_version=None,
local_path=None,
remote_path=None,
)