Fix "borgmatic create --progress" output so that it updates on the console in real-time (#221).

This commit is contained in:
Dan Helfman 2019-09-25 12:03:10 -07:00
parent a472735616
commit a897ffd514
10 changed files with 90 additions and 24 deletions

3
NEWS
View file

@ -1,6 +1,7 @@
1.3.20.dev0 1.3.20
* #205: More robust sample systemd service: boot delay, network dependency, lowered CPU/IO * #205: More robust sample systemd service: boot delay, network dependency, lowered CPU/IO
priority, etc. priority, etc.
* #221: Fix "borgmatic create --progress" output so that it updates on the console in real-time.
1.3.19 1.3.19
* #219: Fix visibility of "borgmatic prune --stats" output. * #219: Fix visibility of "borgmatic prune --stats" output.

View file

@ -4,7 +4,7 @@ import logging
import os import os
import tempfile import tempfile
from borgmatic.execute import execute_command from borgmatic.execute import execute_command, execute_command_without_capture
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -163,6 +163,12 @@ def create_archive(
+ sources + sources
) )
# The progress output isn't compatible with captured and logged output, as progress messes with
# the terminal directly.
if progress:
execute_command_without_capture(full_command)
return
if json: if json:
output_log_level = None output_log_level = None
elif stats: elif stats:

View file

@ -1,6 +1,6 @@
import logging import logging
from borgmatic.execute import execute_command from borgmatic.execute import execute_command, execute_command_without_capture
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -83,4 +83,10 @@ def extract_archive(
+ (tuple(restore_paths) if restore_paths else ()) + (tuple(restore_paths) if restore_paths else ())
) )
# The progress output isn't compatible with captured and logged output, as progress messes with
# the terminal directly.
if progress:
execute_command_without_capture(full_command)
return
execute_command(full_command) execute_command(full_command)

View file

@ -1,7 +1,7 @@
import logging import logging
import subprocess import subprocess
from borgmatic.execute import BORG_ERROR_EXIT_CODE, execute_command from borgmatic.execute import execute_command, execute_command_without_capture
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -45,8 +45,4 @@ def initialize_repository(
) )
# Don't use execute_command() here because it doesn't support interactive prompts. # Don't use execute_command() here because it doesn't support interactive prompts.
try: execute_command_without_capture(init_command)
subprocess.check_call(init_command)
except subprocess.CalledProcessError as error:
if error.returncode >= BORG_ERROR_EXIT_CODE:
raise

View file

@ -61,3 +61,18 @@ def execute_command(full_command, output_log_level=logging.INFO, shell=False):
return output.decode() if output is not None else None return output.decode() if output is not None else None
else: else:
execute_and_log_output(full_command, output_log_level, shell=shell) execute_and_log_output(full_command, output_log_level, shell=shell)
def execute_command_without_capture(full_command):
'''
Execute the given command (a sequence of command/argument strings), but don't capture or log its
output in any way. This is necessary for commands that monkey with the terminal (e.g. progress
display) or provide interactive prompts.
'''
logger.debug(' '.join(full_command))
try:
subprocess.check_call(full_command)
except subprocess.CalledProcessError as error:
if error.returncode >= BORG_ERROR_EXIT_CODE:
raise

View file

@ -1,6 +1,6 @@
from setuptools import find_packages, setup from setuptools import find_packages, setup
VERSION = '1.3.20.dev0' VERSION = '1.3.20'
setup( setup(

View file

@ -758,6 +758,29 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter():
) )
def test_create_archive_with_progress_calls_borg_with_progress_parameter():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
flexmock(module).should_receive('_write_pattern_file').and_return(None)
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command_without_capture').with_args(
('borg', 'create', '--progress') + ARCHIVE_WITH_PATHS
)
module.create_archive(
dry_run=False,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={},
progress=True,
)
def test_create_archive_with_json_calls_borg_with_json_parameter(): def test_create_archive_with_json_calls_borg_with_json_parameter():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(()) flexmock(module).should_receive('_expand_home_directories').and_return(())

View file

@ -195,7 +195,9 @@ def test_extract_archive_calls_borg_with_dry_run_parameter():
def test_extract_archive_calls_borg_with_progress_parameter(): def test_extract_archive_calls_borg_with_progress_parameter():
insert_execute_command_mock(('borg', 'extract', '--progress', 'repo::archive')) flexmock(module).should_receive('execute_command_without_capture').with_args(
('borg', 'extract', '--progress', 'repo::archive')
).once()
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,

View file

@ -23,8 +23,8 @@ def insert_info_command_not_found_mock():
def insert_init_command_mock(init_command, **kwargs): def insert_init_command_mock(init_command, **kwargs):
flexmock(module.subprocess).should_receive('check_call').with_args( flexmock(module).should_receive('execute_command_without_capture').with_args(
init_command, **kwargs init_command
).once() ).once()
@ -35,18 +35,9 @@ def test_initialize_repository_calls_borg_with_parameters():
module.initialize_repository(repository='repo', encryption_mode='repokey') module.initialize_repository(repository='repo', encryption_mode='repokey')
def test_initialize_repository_does_not_raise_for_borg_init_warning():
insert_info_command_not_found_mock()
flexmock(module.subprocess).should_receive('check_call').and_raise(
module.subprocess.CalledProcessError(1, 'borg init')
)
module.initialize_repository(repository='repo', encryption_mode='repokey')
def test_initialize_repository_raises_for_borg_init_error(): def test_initialize_repository_raises_for_borg_init_error():
insert_info_command_not_found_mock() insert_info_command_not_found_mock()
flexmock(module.subprocess).should_receive('check_call').and_raise( flexmock(module).should_receive('execute_command_without_capture').and_raise(
module.subprocess.CalledProcessError(2, 'borg init') module.subprocess.CalledProcessError(2, 'borg init')
) )
@ -56,7 +47,7 @@ def test_initialize_repository_raises_for_borg_init_error():
def test_initialize_repository_skips_initialization_when_repository_already_exists(): def test_initialize_repository_skips_initialization_when_repository_already_exists():
insert_info_command_found_mock() insert_info_command_found_mock()
flexmock(module.subprocess).should_receive('check_call').never() flexmock(module).should_receive('execute_command_without_capture').never()
module.initialize_repository(repository='repo', encryption_mode='repokey') module.initialize_repository(repository='repo', encryption_mode='repokey')

View file

@ -1,5 +1,6 @@
import logging import logging
import pytest
from flexmock import flexmock from flexmock import flexmock
from borgmatic import execute as module from borgmatic import execute as module
@ -49,3 +50,28 @@ def test_execute_command_captures_output_with_shell():
output = module.execute_command(full_command, output_log_level=None, shell=True) output = module.execute_command(full_command, output_log_level=None, shell=True)
assert output == expected_output assert output == expected_output
def test_execute_command_without_capture_does_not_raise_on_success():
flexmock(module.subprocess).should_receive('check_call').and_raise(
module.subprocess.CalledProcessError(0, 'borg init')
)
module.execute_command_without_capture(('borg', 'init'))
def test_execute_command_without_capture_does_not_raise_on_warning():
flexmock(module.subprocess).should_receive('check_call').and_raise(
module.subprocess.CalledProcessError(1, 'borg init')
)
module.execute_command_without_capture(('borg', 'init'))
def test_execute_command_without_capture_raises_on_error():
flexmock(module.subprocess).should_receive('check_call').and_raise(
module.subprocess.CalledProcessError(2, 'borg init')
)
with pytest.raises(module.subprocess.CalledProcessError):
module.execute_command_without_capture(('borg', 'init'))