Prechádzať zdrojové kódy

Add test coverage for new code.

Dan Helfman 5 mesiacov pred
rodič
commit
bbf1c3d55e

+ 28 - 26
borgmatic/actions/create.py

@@ -43,44 +43,46 @@ def collect_patterns(config):
     directories, etc., but we'd like to collapse them all down to one common format (patterns) for
     directories, etc., but we'd like to collapse them all down to one common format (patterns) for
     ease of manipulation within borgmatic.
     ease of manipulation within borgmatic.
     '''
     '''
-    return (
-        tuple(
-            borgmatic.borg.pattern.Pattern(source_directory)
-            for source_directory in config.get('source_directories', ())
-        )
-        + tuple(
-            parse_pattern(pattern_line)
-            for pattern_line in config.get('patterns', ())
-            if not pattern_line.lstrip().startswith('#')
-        )
-        + tuple(
-            borgmatic.borg.pattern.Pattern(
-                exclude_line,
-                borgmatic.borg.pattern.Pattern_type.EXCLUDE,
-                borgmatic.borg.pattern.Pattern_style.FNMATCH,
+    try:
+        return (
+            tuple(
+                borgmatic.borg.pattern.Pattern(source_directory)
+                for source_directory in config.get('source_directories', ())
             )
             )
-            for exclude_line in config.get('exclude_patterns', ())
-        )
-        + tuple(
-            itertools.chain.from_iterable(
-                parse_pattern(pattern_line)
+            + tuple(
+                parse_pattern(pattern_line.strip())
+                for pattern_line in config.get('patterns', ())
+                if not pattern_line.lstrip().startswith('#')
+            )
+            + tuple(
+                borgmatic.borg.pattern.Pattern(
+                    exclude_line.strip(),
+                    borgmatic.borg.pattern.Pattern_type.EXCLUDE,
+                    borgmatic.borg.pattern.Pattern_style.FNMATCH,
+                )
+                for exclude_line in config.get('exclude_patterns', ())
+            )
+            + tuple(
+                parse_pattern(pattern_line.strip())
                 for filename in config.get('patterns_from', ())
                 for filename in config.get('patterns_from', ())
                 for pattern_line in open(filename).readlines()
                 for pattern_line in open(filename).readlines()
                 if not pattern_line.lstrip().startswith('#')
                 if not pattern_line.lstrip().startswith('#')
             )
             )
-        )
-        + tuple(
-            itertools.chain.from_iterable(
+            + tuple(
                 borgmatic.borg.pattern.Pattern(
                 borgmatic.borg.pattern.Pattern(
-                    exclude_line,
+                    exclude_line.strip(),
                     borgmatic.borg.pattern.Pattern_type.EXCLUDE,
                     borgmatic.borg.pattern.Pattern_type.EXCLUDE,
-                    borgmatic.borg.pattern.Pattern_type.FNMATCH,
+                    borgmatic.borg.pattern.Pattern_style.FNMATCH,
                 )
                 )
                 for filename in config.get('excludes_from', ())
                 for filename in config.get('excludes_from', ())
                 for exclude_line in open(filename).readlines()
                 for exclude_line in open(filename).readlines()
+                if not exclude_line.lstrip().startswith('#')
             )
             )
         )
         )
-    )
+    except (FileNotFoundError, OSError) as error:
+        logger.debug(error)
+
+        raise ValueError(f'Cannot read patterns_from/excludes_from file: {error.filename}')
 
 
 
 
 def expand_directory(directory, working_directory):
 def expand_directory(directory, working_directory):

+ 2 - 1
borgmatic/borg/create.py

@@ -337,8 +337,9 @@ def make_base_create_command(
                 log_prefix=repository_path,
                 log_prefix=repository_path,
                 patterns_file=patterns_file,
                 patterns_file=patterns_file,
             )
             )
+
             if '--patterns-from' not in create_flags:
             if '--patterns-from' not in create_flags:
-                create_flags.append(('--patterns-from', patterns_file.name))
+                create_flags += ('--patterns-from', patterns_file.name)
 
 
     return (create_flags, create_positional_arguments, patterns_file)
     return (create_flags, create_positional_arguments, patterns_file)
 
 

+ 100 - 1
tests/unit/actions/test_create.py

@@ -1,8 +1,104 @@
+import io
+import sys
+
 import pytest
 import pytest
 from flexmock import flexmock
 from flexmock import flexmock
 
 
 from borgmatic.actions import create as module
 from borgmatic.actions import create as module
-from borgmatic.borg.pattern import Pattern, Pattern_type
+from borgmatic.borg.pattern import Pattern, Pattern_style, Pattern_type
+
+
+@pytest.mark.parametrize(
+    'pattern_line,expected_pattern',
+    (
+        ('R /foo', Pattern('/foo')),
+        ('P sh', Pattern('sh', Pattern_type.PATTERN_STYLE)),
+        ('+ /foo*', Pattern('/foo*', Pattern_type.INCLUDE)),
+        ('+ sh:/foo*', Pattern('/foo*', Pattern_type.INCLUDE, Pattern_style.SHELL)),
+    ),
+)
+def test_parse_pattern_transforms_pattern_line_to_instance(pattern_line, expected_pattern):
+    module.parse_pattern(pattern_line) == expected_pattern
+
+
+def test_parse_pattern_with_invalid_pattern_line_errors():
+    with pytest.raises(ValueError):
+        module.parse_pattern('/foo')
+
+
+def test_collect_patterns_converts_source_directories():
+    assert module.collect_patterns({'source_directories': ['/foo', '/bar']}) == (
+        Pattern('/foo'),
+        Pattern('/bar'),
+    )
+
+
+def test_collect_patterns_parses_config_patterns():
+    flexmock(module).should_receive('parse_pattern').with_args('R /foo').and_return(Pattern('/foo'))
+    flexmock(module).should_receive('parse_pattern').with_args('# comment').never()
+    flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar'))
+
+    assert module.collect_patterns({'patterns': ['R /foo', '# comment', 'R /bar']}) == (
+        Pattern('/foo'),
+        Pattern('/bar'),
+    )
+
+
+def test_collect_patterns_converts_exclude_patterns():
+    assert module.collect_patterns({'exclude_patterns': ['/foo', '/bar']}) == (
+        Pattern('/foo', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
+        Pattern('/bar', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
+    )
+
+
+def test_collect_patterns_reads_config_patterns_from_file():
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_receive('open').with_args('file1.txt').and_return(io.StringIO('R /foo'))
+    builtins.should_receive('open').with_args('file2.txt').and_return(
+        io.StringIO('R /bar\n# comment\nR /baz')
+    )
+    flexmock(module).should_receive('parse_pattern').with_args('R /foo').and_return(Pattern('/foo'))
+    flexmock(module).should_receive('parse_pattern').with_args('# comment').never()
+    flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar'))
+    flexmock(module).should_receive('parse_pattern').with_args('R /baz').and_return(Pattern('/baz'))
+
+    assert module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']}) == (
+        Pattern('/foo'),
+        Pattern('/bar'),
+        Pattern('/baz'),
+    )
+
+
+def test_collect_patterns_errors_on_missing_config_patterns_from_file():
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_receive('open').with_args('file1.txt').and_raise(FileNotFoundError)
+    flexmock(module).should_receive('parse_pattern').never()
+
+    with pytest.raises(ValueError):
+        module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']})
+
+
+def test_collect_patterns_reads_config_excludes_from_file():
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_receive('open').with_args('file1.txt').and_return(io.StringIO('/foo'))
+    builtins.should_receive('open').with_args('file2.txt').and_return(
+        io.StringIO('/bar\n# comment\n/baz')
+    )
+
+    assert module.collect_patterns({'excludes_from': ['file1.txt', 'file2.txt']}) == (
+        Pattern('/foo', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
+        Pattern('/bar', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
+        Pattern('/baz', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
+    )
+
+
+def test_collect_patterns_errors_on_missing_config_excludes_from_file():
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_receive('open').with_args('file1.txt').and_raise(OSError)
+    flexmock(module).should_receive('parse_pattern').never()
+
+    with pytest.raises(ValueError):
+        module.collect_patterns({'excludes_from': ['file1.txt', 'file2.txt']})
 
 
 
 
 def test_expand_directory_with_basic_path_passes_it_through():
 def test_expand_directory_with_basic_path_passes_it_through():
@@ -275,6 +371,7 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
         'call_hooks_even_if_unconfigured'
         'call_hooks_even_if_unconfigured'
     ).and_return({})
     ).and_return({})
+    flexmock(module).should_receive('collect_patterns').and_return(())
     flexmock(module).should_receive('process_patterns').and_return([])
     flexmock(module).should_receive('process_patterns').and_return([])
     flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
     flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
     create_arguments = flexmock(
     create_arguments = flexmock(
@@ -317,6 +414,7 @@ def test_run_create_runs_with_selected_repository():
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
         'call_hooks_even_if_unconfigured'
         'call_hooks_even_if_unconfigured'
     ).and_return({})
     ).and_return({})
+    flexmock(module).should_receive('collect_patterns').and_return(())
     flexmock(module).should_receive('process_patterns').and_return([])
     flexmock(module).should_receive('process_patterns').and_return([])
     flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
     flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
     create_arguments = flexmock(
     create_arguments = flexmock(
@@ -396,6 +494,7 @@ def test_run_create_produces_json():
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
         'call_hooks_even_if_unconfigured'
         'call_hooks_even_if_unconfigured'
     ).and_return({})
     ).and_return({})
+    flexmock(module).should_receive('collect_patterns').and_return(())
     flexmock(module).should_receive('process_patterns').and_return([])
     flexmock(module).should_receive('process_patterns').and_return([])
     flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
     flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
     create_arguments = flexmock(
     create_arguments = flexmock(

+ 71 - 37
tests/unit/borg/test_create.py

@@ -4,7 +4,7 @@ import pytest
 from flexmock import flexmock
 from flexmock import flexmock
 
 
 from borgmatic.borg import create as module
 from borgmatic.borg import create as module
-from borgmatic.borg.pattern import Pattern, Pattern_type
+from borgmatic.borg.pattern import Pattern, Pattern_style, Pattern_type
 
 
 from ..test_verbosity import insert_logging_mock
 from ..test_verbosity import insert_logging_mock
 
 
@@ -418,38 +418,6 @@ def test_make_base_create_command_with_store_config_false_omits_config_files():
     assert not pattern_file
     assert not pattern_file
 
 
 
 
-def test_make_base_create_command_includes_exclude_patterns_in_borg_command():
-    flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
-    flexmock(module).should_receive('write_patterns_file').and_return(flexmock(name='patterns'))
-    flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
-    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
-        '{hostname}'
-    )
-    flexmock(module.feature).should_receive('available').and_return(True)
-    flexmock(module).should_receive('make_exclude_flags').and_return(())
-    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
-        (f'repo::{DEFAULT_ARCHIVE_NAME}',)
-    )
-
-    (create_flags, create_positional_arguments, pattern_file) = module.make_base_create_command(
-        dry_run=False,
-        repository_path='repo',
-        config={
-            'source_directories': ['foo', 'bar'],
-            'repositories': ['repo'],
-            'exclude_patterns': ['exclude'],
-        },
-        patterns=[Pattern('foo'), Pattern('bar')],
-        local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False),
-        borgmatic_runtime_directory='/run/borgmatic',
-    )
-
-    assert create_flags == ('borg', 'create', '--patterns-from', 'patterns')
-    assert create_positional_arguments == REPO_ARCHIVE
-    assert pattern_file
-
-
 @pytest.mark.parametrize(
 @pytest.mark.parametrize(
     'option_name,option_value,feature_available,option_flags',
     'option_name,option_value,feature_available,option_flags',
     (
     (
@@ -675,8 +643,12 @@ def test_make_base_create_command_includes_list_flags_in_borg_command():
 
 
 
 
 def test_make_base_create_command_with_stream_processes_ignores_read_special_false_and_excludes_special_files():
 def test_make_base_create_command_with_stream_processes_ignores_read_special_false_and_excludes_special_files():
+    patterns = [Pattern('foo'), Pattern('bar')]
+    patterns_file = flexmock(name='patterns')
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
-    flexmock(module).should_receive('write_patterns_file').and_return(None)
+    flexmock(module).should_receive('write_patterns_file').with_args(
+        patterns, '/run/borgmatic', object
+    ).and_return(patterns_file)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
     flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
         '{hostname}'
         '{hostname}'
@@ -689,7 +661,18 @@ def test_make_base_create_command_with_stream_processes_ignores_read_special_fal
     flexmock(module.logger).should_receive('warning').twice()
     flexmock(module.logger).should_receive('warning').twice()
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('collect_special_file_paths').and_return(('/dev/null',)).once()
     flexmock(module).should_receive('collect_special_file_paths').and_return(('/dev/null',)).once()
-    flexmock(module).should_receive('write_patterns_file').and_return(flexmock(name='patterns'))
+    flexmock(module).should_receive('write_patterns_file').with_args(
+        (
+            Pattern(
+                '/dev/null',
+                Pattern_type.EXCLUDE,
+                Pattern_style.FNMATCH,
+            ),
+        ),
+        '/run/borgmatic',
+        'repo',
+        patterns_file=patterns_file,
+    ).and_return(patterns_file).once()
     flexmock(module).should_receive('make_exclude_flags').and_return(())
     flexmock(module).should_receive('make_exclude_flags').and_return(())
 
 
     (create_flags, create_positional_arguments, pattern_file) = module.make_base_create_command(
     (create_flags, create_positional_arguments, pattern_file) = module.make_base_create_command(
@@ -700,7 +683,7 @@ def test_make_base_create_command_with_stream_processes_ignores_read_special_fal
             'repositories': ['repo'],
             'repositories': ['repo'],
             'read_special': False,
             'read_special': False,
         },
         },
-        patterns=[Pattern('foo'), Pattern('bar')],
+        patterns=patterns,
         local_borg_version='1.2.3',
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
         global_arguments=flexmock(log_json=False),
         borgmatic_runtime_directory='/run/borgmatic',
         borgmatic_runtime_directory='/run/borgmatic',
@@ -712,7 +695,58 @@ def test_make_base_create_command_with_stream_processes_ignores_read_special_fal
     assert pattern_file
     assert pattern_file
 
 
 
 
-def test_make_base_create_command_with_stream_processes_and_read_special_true_skip_special_files_excludes():
+def test_make_base_create_command_without_patterns_and_with_stream_processes_ignores_read_special_false_and_excludes_special_files():
+    flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
+    flexmock(module).should_receive('write_patterns_file').with_args(
+        [], '/run/borgmatic', object
+    ).and_return(None)
+    flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
+    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module).should_receive('make_exclude_flags').and_return(())
+    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
+        (f'repo::{DEFAULT_ARCHIVE_NAME}',)
+    )
+    flexmock(module.logger).should_receive('warning').twice()
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('collect_special_file_paths').and_return(('/dev/null',)).once()
+    flexmock(module).should_receive('write_patterns_file').with_args(
+        (
+            Pattern(
+                '/dev/null',
+                Pattern_type.EXCLUDE,
+                Pattern_style.FNMATCH,
+            ),
+        ),
+        '/run/borgmatic',
+        'repo',
+        patterns_file=None,
+    ).and_return(flexmock(name='patterns')).once()
+    flexmock(module).should_receive('make_exclude_flags').and_return(())
+
+    (create_flags, create_positional_arguments, pattern_file) = module.make_base_create_command(
+        dry_run=False,
+        repository_path='repo',
+        config={
+            'source_directories': [],
+            'repositories': ['repo'],
+            'read_special': False,
+        },
+        patterns=[],
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/run/borgmatic',
+        stream_processes=flexmock(),
+    )
+
+    assert create_flags == ('borg', 'create', '--read-special', '--patterns-from', 'patterns')
+    assert create_positional_arguments == REPO_ARCHIVE
+    assert pattern_file
+
+
+def test_make_base_create_command_with_stream_processes_and_read_special_true_skips_special_files_excludes():
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module).should_receive('write_patterns_file').and_return(None)
     flexmock(module).should_receive('write_patterns_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')