Merge lp:~aaron-whitehouse/duplicity/Bug_1624725_files_within_folder_slash into lp:~duplicity-team/duplicity/0.7-series

Proposed by Aaron Whitehouse
Status: Merged
Merged at revision: 1257
Proposed branch: lp:~aaron-whitehouse/duplicity/Bug_1624725_files_within_folder_slash
Merge into: lp:~duplicity-team/duplicity/0.7-series
Diff against target: 481 lines (+365/-15)
5 files modified
duplicity/globmatch.py (+37/-8)
duplicity/selection.py (+7/-2)
testing/functional/test_selection.py (+76/-1)
testing/unit/test_globmatch.py (+215/-2)
testing/unit/test_selection.py (+30/-2)
To merge this branch: bzr merge lp:~aaron-whitehouse/duplicity/Bug_1624725_files_within_folder_slash
Reviewer Review Type Date Requested Status
duplicity-team Pending
Review via email: mp+311357@code.launchpad.net

Commit message

Fixed Bug #1624725, so that an include glob ending in "/" now includes folder contents (for globs with and without special characters). This preserves the behaviour that an expression ending in "/" only matches a folder, but now the contents of any matching folder is included.

Description of the change

Fixed Bug #1624725, so that an include glob ending in "/" now includes folder contents (for globs with and without special characters). This preserves the behaviour that an expression ending in "/" only matches a folder, but now the contents of any matching folder is included.

Note that this is to be merged into 0.7-series, as this fixes a regression in recent 0.7 releases. The change (likely along with all other changes that have been made in 0.7 recently) could then be merged into 0.8. Once merged into the 0.8 series, I plan to converge the functions dealing with expressions the do and don't have globbing characters so that we only have one code base to check/maintain.

To post a comment you must log in.
Revision history for this message
Aaron Whitehouse (aaron-whitehouse) wrote :

Kenneth, is there anything more I can do to get this merged? Would be good to get this fixed, so let me know if there is more I can do to help.

Revision history for this message
Kenneth Loafman (kenneth-loafman) wrote :

Sorry, this got lost in my spam folder. Not good. Will get it done.

Revision history for this message
Kenneth Loafman (kenneth-loafman) wrote :

BTW, the diff from the merge worked on series-8 as well.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'duplicity/globmatch.py'
2--- duplicity/globmatch.py 2016-08-12 20:50:29 +0000
3+++ duplicity/globmatch.py 2016-11-20 22:26:00 +0000
4@@ -60,11 +60,10 @@
5 2 - if the folder should be scanned for any included/excluded files
6 None - if the selection function has nothing to say about the file
7 """
8- match_only_dirs = False
9+ glob_ends_w_slash = False
10
11- # ToDo: Test behaviour of "/" on its own - should always match
12 if glob_str != "/" and glob_str[-1] == "/":
13- match_only_dirs = True
14+ glob_ends_w_slash = True
15 # Remove trailing / from directory name (unless that is the entire
16 # string)
17 glob_str = glob_str[:-1]
18@@ -76,21 +75,51 @@
19 re_comp = lambda r: re.compile(r, re.S | flags)
20
21 # matches what glob matches and any files in directory
22+ # Resulting regular expression is:
23+ # ^ string must be at the beginning of path
24+ # string translated into regex
25+ # ($|/) nothing must follow except for the end of the string, newline or /
26+ # Note that the "/" at the end of the regex means that it will match
27+ # if the glob matches a parent folders of path
28 glob_comp_re = re_comp("^%s($|/)" % glob_to_regex(glob_str))
29
30+ if glob_ends_w_slash:
31+ # Creates a version of glob_comp_re that does not match folder contents
32+ # This can be used later to check that an exact match is actually a
33+ # folder, rather than a file.
34+ glob_comp_re_exact = re_comp("^%s($)" % glob_to_regex(glob_str))
35+
36 if glob_str.find("**") != -1:
37+ # glob_str has a ** in it
38 glob_str = glob_str[:glob_str.find("**") + 2] # truncate after **
39
40+ # Below regex is translates to:
41+ # ^ string must be at the beginning of path
42+ # the regexs corresponding to the parent directories of glob_str
43+ # $ nothing must follow except for the end of the string or newline
44 scan_comp_re = re_comp("^(%s)$" %
45 "|".join(_glob_get_prefix_regexs(glob_str)))
46
47 def test_fn(path):
48+ if glob_comp_re.match(path.name):
49+ # Path matches glob, or is contained within a matching folder
50+ if not glob_ends_w_slash:
51+ return include
52+ else:
53+ # Glob ended with a /, so we need to check any exact match was
54+ # a folder
55+ if glob_comp_re_exact.match(path.name):
56+ # Not an included file/folder, so must be a folder to match
57+ if path.isdir():
58+ # Is a directory, so all is well
59+ return include
60+ else:
61+ # Exact match and not a folder
62+ return None
63+ else:
64+ # An included file/folder, so normal approach is fine
65+ return include
66
67- if match_only_dirs and not path.isdir():
68- # If the glob ended with a /, only match directories
69- return None
70- elif glob_comp_re.match(path.name):
71- return include
72 elif include == 1 and scan_comp_re.match(path.name):
73 return 2
74 else:
75
76=== modified file 'duplicity/selection.py'
77--- duplicity/selection.py 2016-09-04 22:25:37 +0000
78+++ duplicity/selection.py 2016-11-20 22:26:00 +0000
79@@ -487,6 +487,9 @@
80
81 """
82 # Internal. Used by glob_get_sf and unit tests.
83+ # ToDo: Make all globbing/non-globbing use same code path
84+ # This distinction has bitten us too many times with bugs in one or
85+ # the other.
86 match_only_dirs = False
87
88 if filename != "/" and filename[-1] == "/":
89@@ -506,8 +509,10 @@
90 # Internal. Used by glob_get_filename_sf.
91
92 def include_sel_func(path):
93- if match_only_dirs and not path.isdir():
94- # If the glob ended with a /, only match directories
95+ if len(tuple) == len(path.index) and match_only_dirs and not path.isdir():
96+ # If we are assessing the actual directory (rather than the
97+ # contents of the directory) and the glob ended with a /,
98+ # only match directories
99 return None
100 elif (path.index == tuple[:len(path.index)] or
101 path.index[:len(tuple)] == tuple):
102
103=== modified file 'testing/functional/test_selection.py'
104--- testing/functional/test_selection.py 2016-09-04 22:25:37 +0000
105+++ testing/functional/test_selection.py 2016-11-20 22:26:00 +0000
106@@ -19,7 +19,6 @@
107 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
108
109 import os
110-import os.path
111 import unittest
112
113 from . import FunctionalTestCase
114@@ -1006,5 +1005,81 @@
115 ['1sub1sub1_file.txt']])
116
117
118+class TestFolderIncludesFiles(IncludeExcludeFunctionalTest):
119+ """ This tests that including a folder includes the files within it"""
120+ # https://bugs.launchpad.net/duplicity/+bug/1624725
121+
122+ def test_includes_files(self):
123+ """This tests that including a folder includes the files within it"""
124+ self.backup("full", "testfiles/select2/1/1sub1",
125+ options=["--include", "testfiles/select2/1/1sub1/1sub1sub1",
126+ "--exclude", "**"])
127+ self.restore()
128+ restore_dir = 'testfiles/restore_out'
129+ restored = self.directory_tree_to_list_of_lists(restore_dir)
130+ self.assertEqual(restored, [['1sub1sub1'],
131+ ['1sub1sub1_file.txt']])
132+
133+ def test_includes_files_trailing_slash(self):
134+ """This tests that including a folder includes the files within it"""
135+ self.backup("full", "testfiles/select2/1/1sub1",
136+ options=["--include", "testfiles/select2/1/1sub1/1sub1sub1/",
137+ "--exclude", "**"])
138+ self.restore()
139+ restore_dir = 'testfiles/restore_out'
140+ restored = self.directory_tree_to_list_of_lists(restore_dir)
141+ self.assertEqual(restored, [['1sub1sub1'],
142+ ['1sub1sub1_file.txt']])
143+
144+ def test_includes_files_trailing_slash_globbing_chars(self):
145+ """Tests folder includes with globbing char and /"""
146+ self.backup("full", "testfiles/select2/1/1sub1",
147+ options=["--include", "testfiles/s?lect2/1/1sub1/1sub1sub1/",
148+ "--exclude", "**"])
149+ self.restore()
150+ restore_dir = 'testfiles/restore_out'
151+ restored = self.directory_tree_to_list_of_lists(restore_dir)
152+ self.assertEqual(restored, [['1sub1sub1'],
153+ ['1sub1sub1_file.txt']])
154+
155+ def test_excludes_files_no_trailing_slash(self):
156+ """This tests that excluding a folder excludes the files within it"""
157+ self.backup("full", "testfiles/select2/1/1sub1",
158+ options=["--exclude", "testfiles/select2/1/1sub1/1sub1sub1",
159+ "--exclude", "testfiles/select2/1/1sub1/1sub1sub2",
160+ "--exclude", "testfiles/select2/1/1sub1/1sub1sub3",
161+ "--include", "testfiles/select2/1/1sub1/1sub1**",
162+ "--exclude", "testfiles/select2/1/1sub1/irrelevant.txt"])
163+ self.restore()
164+ restore_dir = 'testfiles/restore_out'
165+ restored = self.directory_tree_to_list_of_lists(restore_dir)
166+ self.assertEqual(restored, [])
167+
168+ def test_excludes_files_trailing_slash(self):
169+ """Excluding a folder excludes the files within it, if ends with /"""
170+ self.backup("full", "testfiles/select2/1/1sub1",
171+ options=["--exclude", "testfiles/select2/1/1sub1/1sub1sub1/",
172+ "--exclude", "testfiles/select2/1/1sub1/1sub1sub2/",
173+ "--exclude", "testfiles/select2/1/1sub1/1sub1sub3/",
174+ "--include", "testfiles/select2/1/1sub1/1sub1**",
175+ "--exclude", "testfiles/select2/1/1sub1/irrelevant.txt"])
176+ self.restore()
177+ restore_dir = 'testfiles/restore_out'
178+ restored = self.directory_tree_to_list_of_lists(restore_dir)
179+ self.assertEqual(restored, [])
180+
181+ def test_excludes_files_trailing_slash_globbing_chars(self):
182+ """Tests folder excludes with globbing char and /"""
183+ self.backup("full", "testfiles/select2/1/1sub1",
184+ options=["--exclude", "testfiles/sel?ct2/1/1sub1/1sub1sub1/",
185+ "--exclude", "testfiles/sel[e,f]ct2/1/1sub1/1sub1sub2/",
186+ "--exclude", "testfiles/sel*t2/1/1sub1/1sub1sub3/",
187+ "--include", "testfiles/select2/1/1sub1/1sub1**",
188+ "--exclude", "testfiles/select2/1/1sub1/irrelevant.txt"])
189+ self.restore()
190+ restore_dir = 'testfiles/restore_out'
191+ restored = self.directory_tree_to_list_of_lists(restore_dir)
192+ self.assertEqual(restored, [])
193+
194 if __name__ == "__main__":
195 unittest.main()
196
197=== modified file 'testing/unit/test_globmatch.py'
198--- testing/unit/test_globmatch.py 2016-03-05 10:47:02 +0000
199+++ testing/unit/test_globmatch.py 2016-11-20 22:26:00 +0000
200@@ -20,9 +20,12 @@
201 # along with duplicity; if not, write to the Free Software Foundation,
202 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
203
204-import unittest
205+import sys
206 from duplicity.globmatch import *
207+from duplicity.path import *
208 from . import UnitTestCase
209+from mock import patch
210+import unittest
211
212
213 class MatchingTest(UnitTestCase):
214@@ -38,4 +41,214 @@
215 assert r == "\\/usr\\/[^/]*\\/bin\\/", r
216 assert glob_to_regex("[a.b/c]") == "[a.b/c]"
217 r = glob_to_regex("[a*b-c]e[!]]")
218- assert r == "[a*b-c]e[^]]", r
219\ No newline at end of file
220+ assert r == "[a*b-c]e[^]]", r
221+
222+
223+class TestDoubleAsteriskOnIncludesExcludes(UnitTestCase):
224+ """Test ** on includes and exclude patterns"""
225+
226+ def test_double_asterisk_include(self):
227+ """Test a few globbing patterns, including **"""
228+ self.assertEqual(
229+ path_matches_glob_fn("**", 1)(Path("foo.txt")), 1)
230+ with patch('duplicity.path.Path.isdir') as mock_isdir:
231+ mock_isdir.return_value = True
232+ self.assertEqual(
233+ path_matches_glob_fn("**", 1)(Path("folder")), 1)
234+
235+ def test_double_asterisk_extension_include(self):
236+ """Test **.py"""
237+ self.assertEqual(
238+ path_matches_glob_fn("**.py", 1)(Path("what/ever.py")), 1)
239+ self.assertEqual(
240+ path_matches_glob_fn("**.py", 1)(Path("what/ever.py/foo")), 1)
241+ with patch('duplicity.path.Path.isdir') as mock_isdir:
242+ mock_isdir.return_value = True
243+ self.assertEqual(
244+ path_matches_glob_fn("**.py", 1)(Path("foo")), 2)
245+ self.assertEqual(
246+ path_matches_glob_fn("**.py", 1)(Path("usr/local/bin")), 2)
247+ self.assertEqual(
248+ path_matches_glob_fn("**.py", 1)(Path("/usr/local/bin")), 2)
249+
250+
251+class TestTrailingSlash(UnitTestCase):
252+ """Test glob matching where the glob has a trailing slash"""
253+
254+ def test_trailing_slash_matches_only_dirs(self):
255+ """Test matching where glob includes a trailing slash"""
256+ with patch('duplicity.path.Path.isdir') as mock_isdir:
257+ mock_isdir.return_value = True
258+ self.assertEqual(
259+ path_matches_glob_fn("fold*/", 1)(Path("folder")), 1)
260+
261+ # Test the file named "folder" is not included if it is not a dir
262+ mock_isdir.return_value = False
263+ self.assertEqual(
264+ path_matches_glob_fn("fold*/", 1)(Path("folder")), None)
265+
266+ # Test the file named "folder" is not included if it is not a dir
267+ mock_isdir.return_value = False
268+ self.assertEqual(
269+ path_matches_glob_fn("folder/", 1)(Path("folder")), None)
270+
271+ mock_isdir.return_value = False
272+ self.assertEqual(
273+ path_matches_glob_fn("fo*/", 1)(Path("foo.txt")), None)
274+
275+ def test_included_files_are_matched_no_slash(self):
276+ """Test that files within an included folder are matched"""
277+ with patch('duplicity.path.Path.isdir') as mock_isdir:
278+ mock_isdir.return_value = False
279+ self.assertEqual(
280+ path_matches_glob_fn("fold*", 1)(Path("folder/file.txt")), 1)
281+
282+ with patch('duplicity.path.Path.isdir') as mock_isdir:
283+ mock_isdir.return_value = False
284+ self.assertEqual(
285+ path_matches_glob_fn("fold*", 1)(Path("folder/2/file.txt")), 1)
286+
287+ def test_included_files_are_matched_no_slash_2(self):
288+ """Test that files within an included folder are matched"""
289+ with patch('duplicity.path.Path.isdir') as mock_isdir:
290+ mock_isdir.return_value = False
291+ self.assertEqual(
292+ path_matches_glob_fn("folder", 1)(Path("folder/file.txt")), 1)
293+
294+ with patch('duplicity.path.Path.isdir') as mock_isdir:
295+ mock_isdir.return_value = False
296+ self.assertEqual(
297+ path_matches_glob_fn("folder/2", 1)(Path("folder/2/file.txt")), 1)
298+
299+ def test_included_files_are_matched_slash(self):
300+ """Test that files within an included folder are matched with /"""
301+ # Bug #1624725
302+ # https://bugs.launchpad.net/duplicity/+bug/1624725
303+ with patch('duplicity.path.Path.isdir') as mock_isdir:
304+ mock_isdir.return_value = False
305+ self.assertEqual(
306+ path_matches_glob_fn("folder/", 1)(Path("folder/file.txt")), 1)
307+
308+ def test_included_files_are_matched_slash_2(self):
309+ """Test that files within an included folder are matched with /"""
310+ # Bug #1624725
311+ # https://bugs.launchpad.net/duplicity/+bug/1624725
312+ with patch('duplicity.path.Path.isdir') as mock_isdir:
313+ mock_isdir.return_value = False
314+ self.assertEqual(
315+ path_matches_glob_fn("testfiles/select2/1/1sub1/1sub1sub1/", 1)
316+ (Path("testfiles/select2/1/1sub1/1sub1sub1/1sub1sub1_file.txt")
317+ ), 1)
318+
319+ def test_included_files_are_matched_slash_2_parents(self):
320+ """Test that duplicity will scan parent of glob/"""
321+ # Bug #1624725
322+ # https://bugs.launchpad.net/duplicity/+bug/1624725
323+ with patch('duplicity.path.Path.isdir') as mock_isdir:
324+ mock_isdir.return_value = True
325+ self.assertEqual(
326+ path_matches_glob_fn("testfiles/select2/1/1sub1/1sub1sub1/", 1)
327+ (Path("testfiles/select2/1/1sub1/1sub1sub1")
328+ ), 1)
329+ self.assertEqual(
330+ path_matches_glob_fn("testfiles/select2/1/1sub1/1sub1sub1/", 1)
331+ (Path("testfiles/select2/1/1sub1")
332+ ), 2)
333+
334+ def test_included_files_are_matched_slash_wildcard(self):
335+ """Test that files within an included folder are matched with /"""
336+ # Bug #1624725
337+ # https://bugs.launchpad.net/duplicity/+bug/1624725
338+ with patch('duplicity.path.Path.isdir') as mock_isdir:
339+ mock_isdir.return_value = False
340+ self.assertEqual(
341+ path_matches_glob_fn("fold*/", 1)(Path("folder/file.txt")), 1)
342+ #
343+ # def test_slash_matches_everything(self):
344+ # """Test / matches everything"""
345+ # # ToDo: Not relevant at this stage, as "/" would not go through
346+ # # globmatch because it has no special characters, but it should be
347+ # # made to work
348+ # with patch('duplicity.path.Path.isdir') as mock_isdir:
349+ # mock_isdir.return_value = True
350+ # self.assertEqual(
351+ # path_matches_glob_fn("/",
352+ # 1)(Path("/tmp/testfiles/select/1/2")), 1)
353+ # self.assertEqual(
354+ # path_matches_glob_fn("/",
355+ # 1)(Path("/test/random/path")), 1)
356+ # self.assertEqual(
357+ # path_matches_glob_fn("/",
358+ # 1)(Path("/")), 1)
359+ # self.assertEqual(
360+ # path_matches_glob_fn("/",
361+ # 1)(Path("/var/log")), 1)
362+ # self.assertEqual(
363+ # path_matches_glob_fn("/",
364+ # 1)(Path("/var/log/log.txt")), 1)
365+
366+ def test_slash_star_scans_folder(self):
367+ """Test that folder/* scans folder/"""
368+ # This behaviour is a bit ambiguous - either include or scan could be
369+ # argued as most appropriate here, but only an empty folder is at stake
370+ # so long as test_slash_star_includes_folder_contents passes.
371+ with patch('duplicity.path.Path.isdir') as mock_isdir:
372+ mock_isdir.return_value = True
373+ self.assertEqual(
374+ path_matches_glob_fn("folder/*", 1)(Path("folder")), 2)
375+
376+ def test_slash_star_includes_folder_contents(self):
377+ """Test that folder/* includes folder contents"""
378+ self.assertEqual(path_matches_glob_fn("folder/*", 1)
379+ (Path("folder/file.txt")), 1)
380+ self.assertEqual(path_matches_glob_fn("folder/*", 1)
381+ (Path("folder/other_file.log")), 1)
382+
383+ def test_slash_star_star_includes_folder(self):
384+ """Test that folder/** includes folder/"""
385+ with patch('duplicity.path.Path.isdir') as mock_isdir:
386+ mock_isdir.return_value = True
387+
388+ def test_simple_trailing_slash_match(self):
389+ """Test that a normal folder string ending in / matches that path"""
390+ with patch('duplicity.path.Path.isdir') as mock_isdir:
391+ mock_isdir.return_value = True
392+ test_path = "testfiles/select/1/2/1"
393+ self.assertEqual(
394+ path_matches_glob_fn(test_path, 1)(Path(test_path)), 1)
395+
396+ def test_double_asterisk_string_slash(self):
397+ """Test string starting with ** and ending in /"""
398+ with patch('duplicity.path.Path.isdir') as mock_isdir:
399+ mock_isdir.return_value = True
400+ self.assertEqual(
401+ path_matches_glob_fn("**/1/2/",
402+ 1)(Path("testfiles/select/1/2")), 1)
403+
404+ def test_string_double_asterisk_string_slash(self):
405+ """Test string ** string /"""
406+ with patch('duplicity.path.Path.isdir') as mock_isdir:
407+ mock_isdir.return_value = True
408+ self.assertEqual(
409+ path_matches_glob_fn("testfiles**/2/",
410+ 1)(Path("testfiles/select/1/2")), 1)
411+
412+
413+class TestDoubleAsterisk(UnitTestCase):
414+ """Test glob matching where the glob finishes with a **"""
415+
416+ def test_simple_trailing_slash_match(self):
417+ """Test that a folder string ending in /** matches that path"""
418+ with patch('duplicity.path.Path.isdir') as mock_isdir:
419+ mock_isdir.return_value = True
420+ self.assertEqual(
421+ path_matches_glob_fn("/test/folder/**", 1)(
422+ Path("/test/foo")), None)
423+
424+ def test_simple_trailing_slash_match_2(self):
425+ """Test folder string ending in */**"""
426+ with patch('duplicity.path.Path.isdir') as mock_isdir:
427+ mock_isdir.return_value = True
428+ self.assertEqual(
429+ path_matches_glob_fn("fold*/**", 1)(
430+ Path("folder")), 2)
431\ No newline at end of file
432
433=== modified file 'testing/unit/test_selection.py'
434--- testing/unit/test_selection.py 2016-03-05 10:14:57 +0000
435+++ testing/unit/test_selection.py 2016-11-20 22:26:00 +0000
436@@ -83,8 +83,6 @@
437
438 with patch('duplicity.path.ROPath.isdir') as mock_isdir:
439 mock_isdir.return_value = True
440- # Can't pass the return_value as an argument to patch, as build
441- # system's mock is too old to support it.
442
443 assert sf2(self.makeext("usr")) is None
444 assert sf2(self.makeext("usr/local")) is None
445@@ -666,6 +664,36 @@
446 ("--exclude", "**t/1/3")],
447 [(), ('2',), ('2', '1')])
448
449+ def test_includes_files(self):
450+ """Unit test the functional test test_includes_files"""
451+ # Test for Bug 1624725
452+ # https://bugs.launchpad.net/duplicity/+bug/1624725
453+ self.root = Path("testfiles/select2/1/1sub1")
454+ self.ParseTest([("--include", "testfiles/select2/1/1sub1/1sub1sub1"),
455+ ("--exclude", "**")],
456+ [(), ('1sub1sub1',), ('1sub1sub1',
457+ '1sub1sub1_file.txt')])
458+
459+ def test_includes_files_trailing_slash(self):
460+ """Unit test the functional test test_includes_files_trailing_slash"""
461+ # Test for Bug 1624725
462+ # https://bugs.launchpad.net/duplicity/+bug/1624725
463+ self.root = Path("testfiles/select2/1/1sub1")
464+ self.ParseTest([("--include", "testfiles/select2/1/1sub1/1sub1sub1/"),
465+ ("--exclude", "**")],
466+ [(), ('1sub1sub1',), ('1sub1sub1',
467+ '1sub1sub1_file.txt')])
468+
469+ def test_includes_files_trailing_slash_globbing_chars(self):
470+ """Unit test functional test_includes_files_trailing_slash_globbing_chars"""
471+ # Test for Bug 1624725
472+ # https://bugs.launchpad.net/duplicity/+bug/1624725
473+ self.root = Path("testfiles/select2/1/1sub1")
474+ self.ParseTest([("--include", "testfiles/s?lect2/1/1sub1/1sub1sub1/"),
475+ ("--exclude", "**")],
476+ [(), ('1sub1sub1',), ('1sub1sub1',
477+ '1sub1sub1_file.txt')])
478+
479 def test_glob(self):
480 """Test globbing expression"""
481 self.ParseTest([("--exclude", "**[3-5]"),

Subscribers

People subscribed via source and target branches