Merge lp:~nataliabidart/ubuntuone-client/tree-deletion-when-prior-conflict into lp:ubuntuone-client

Proposed by Natalia Bidart
Status: Merged
Approved by: Guillermo Gonzalez
Approved revision: 433
Merged at revision: not available
Proposed branch: lp:~nataliabidart/ubuntuone-client/tree-deletion-when-prior-conflict
Merge into: lp:ubuntuone-client
Diff against target: 262 lines (+102/-56)
4 files modified
contrib/testing/testcase.py (+24/-0)
tests/syncdaemon/test_fsm.py (+58/-22)
tests/syncdaemon/test_states.py (+1/-21)
ubuntuone/syncdaemon/filesystem_manager.py (+19/-13)
To merge this branch: bzr merge lp:~nataliabidart/ubuntuone-client/tree-deletion-when-prior-conflict
Reviewer Review Type Date Requested Status
Guillermo Gonzalez Approve
Roman Yepishev (community) Approve
Review via email: mp+21395@code.launchpad.net

Commit message

Fix for Bug #462003: when there is a server-side tree deletion and the user already have conflicts in the removed directory, the directory itself is moved to conflict.

Description of the change

  Added test case and fix for Bug #462003 (when there is a server-side tree deletion and the user already have conflicts in the removed directory, the directory itself is moved to conflict).

  Added simple fix to pseudo-related Bug #494469.

To post a comment you must log in.
430. By Natalia Bidart

Fixing lint issues.

431. By Natalia Bidart

Using MementoHandler to assert over logs.

432. By Natalia Bidart

Restoring previous code to avoid diffs.

Revision history for this message
Roman Yepishev (rye) wrote :

I believe this code will fail in case the whole dir is renamed to u1conflict. In this case the folder itself will not be checked and it will be removed along with all the files inside.

Based on pydoc:
dirnames is a list of the names of the subdirectories in dirpath (excluding '.' and '..').
filenames is a list of the names of the non-directory files in dirpath.

review: Needs Fixing
433. By Natalia Bidart

Adding test case and code to correctly detect conflicted directories and avoid
tree deletion.

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

> I believe this code will fail in case the whole dir is renamed to u1conflict.
> In this case the folder itself will not be checked and it will be removed
> along with all the files inside.
>
> Based on pydoc:
> dirnames is a list of the names of the subdirectories in dirpath (excluding
> '.' and '..').
> filenames is a list of the names of the non-directory files in dirpath.

Good catch! I added a test case and the proper code for this case. Let me know what you think.

Revision history for this message
Roman Yepishev (rye) wrote :

Tested with folders and dirs, files and directories in local-only state do not get removed on parent tree removal. The parent folder (i.e. which is asked to be removed by the server) is renamed to u1conflict.

I like this :)

review: Approve
Revision history for this message
Guillermo Gonzalez (verterok) wrote :

I'm not sure that using os.walk is the "right thing to do"(tm) here.
Because if the directory tree is *big* this will consume a considerable amount of memory, what do you think about using os.listdir() on path and each subdir?

We need to measure if this is a real problem, or at least track this issue with a Bug.

Could you file a bug about this?

Thanks!

review: Approve
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

> I'm not sure that using os.walk is the "right thing to do"(tm) here.
> Because if the directory tree is *big* this will consume a considerable amount
> of memory, what do you think about using os.listdir() on path and each subdir?
>
> We need to measure if this is a real problem, or at least track this issue
> with a Bug.
>
> Could you file a bug about this?
>
> Thanks!

I reported Bug #539659 regarding this issue.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'contrib/testing/testcase.py'
2--- contrib/testing/testcase.py 2010-03-05 17:41:17 +0000
3+++ contrib/testing/testcase.py 2010-03-15 23:15:33 +0000
4@@ -616,6 +616,30 @@
5 self.remove_from_connection()
6
7
8+class FakeLogger(object):
9+ """Helper logging class."""
10+ def __init__(self):
11+ self.logged = dict(debug=[], warning=[], info=[])
12+
13+ def _log(self, log, txt, args):
14+ """Really logs."""
15+ if args:
16+ txt = txt % args
17+ log.append(txt)
18+
19+ def warning(self, txt, *args):
20+ """WARNING logs."""
21+ self._log(self.logged['warning'], txt, args)
22+
23+ def debug(self, txt, *args):
24+ """DEBUG logs."""
25+ self._log(self.logged['debug'], txt, args)
26+
27+ def info(self, txt, *args):
28+ """INFO logs."""
29+ self._log(self.logged['info'], txt, args)
30+
31+
32 class Listener(object):
33 """Helper class to gather events."""
34
35
36=== modified file 'tests/syncdaemon/test_fsm.py'
37--- tests/syncdaemon/test_fsm.py 2010-02-10 17:35:26 +0000
38+++ tests/syncdaemon/test_fsm.py 2010-03-15 23:15:33 +0000
39@@ -24,12 +24,11 @@
40 import shutil
41 import unittest
42 import time
43-import logging
44
45 from contrib.testing.testcase import (
46 FakeVolumeManager,
47 FakeMain,
48- MementoHandler,
49+ MementoHandler
50 )
51
52 from ubuntuone.syncdaemon.filesystem_manager import (
53@@ -39,7 +38,6 @@
54 METADATA_VERSION,
55 )
56 from ubuntuone.syncdaemon.event_queue import EventQueue
57-from ubuntuone.syncdaemon.logger import LOGFILENAME
58 from ubuntuone.syncdaemon import logger
59 from ubuntuone.syncdaemon.volume_manager import Share, allow_writes
60
61@@ -74,11 +72,19 @@
62 self.fsm, self.shares_dir)
63 self.share_path = self.share.path
64
65+ # add a in-memory logger handler
66+ self.memento = MementoHandler()
67+ self.memento.setLevel(0)
68+ logger.root_logger.addHandler(self.memento)
69+
70 def tearDown(self):
71 """ Clean up the tests. """
72 self.eq.shutdown()
73 self.rmtree(TESTS_DIR)
74
75+ # remove the handler
76+ logger.root_logger.removeHandler(self.memento)
77+
78 @staticmethod
79 def rmtree(path):
80 """rmtree that handle ro childs."""
81@@ -1866,13 +1872,48 @@
82 assert self.fsm.changed(path=local_file) == self.fsm.CHANGED_LOCAL
83 self.assertRaises(OSError, self.fsm.delete_file, local_dir)
84
85+ def test_delete_dir_when_non_empty_and_prior_conflict_on_file(self):
86+ """Test that a dir is not deleted, when there is a conflicted file."""
87+ # local directory
88+ local_dir = os.path.join(self.root_dir, "foo")
89+ os.mkdir(local_dir)
90+ self.fsm.create(local_dir, "", is_dir=True)
91+ self.fsm.set_node_id(local_dir, "uuid")
92+
93+ local_file = os.path.join(local_dir,
94+ "bar.txt" + self.fsm.CONFLICT_SUFFIX)
95+ open(local_file, 'w').close() # touch bar.txt.u1conflict
96+
97+ assert not local_file in self.fsm._idx_path
98+ self.assertRaises(OSError, self.fsm.delete_file, local_dir)
99+
100+ infos = [record.message for record in self.memento.records
101+ if record.levelname == 'INFO']
102+ self.assertTrue(len(infos) == 1)
103+ self.assertTrue(local_file in infos[0])
104+
105+ def test_delete_dir_when_non_empty_and_prior_conflict_on_subdir(self):
106+ """Test that a dir is not deleted, when there is a conflicted dir."""
107+ # local directory
108+ local_dir = os.path.join(self.root_dir, "foo")
109+ os.mkdir(local_dir)
110+ self.fsm.create(local_dir, "", is_dir=True)
111+ self.fsm.set_node_id(local_dir, "uuid")
112+
113+ local_subdir = os.path.join(local_dir,
114+ "subdir_bar" + self.fsm.CONFLICT_SUFFIX)
115+ os.mkdir(local_subdir)
116+
117+ assert not local_subdir in self.fsm._idx_path
118+ self.assertRaises(OSError, self.fsm.delete_file, local_dir)
119+
120+ infos = [record.message for record in self.memento.records
121+ if record.levelname == 'INFO']
122+ self.assertTrue(len(infos) == 1)
123+ self.assertTrue(local_subdir in infos[0])
124+
125 def test_no_warning_on_log_file_when_recursive_delete(self):
126 """Test that sucessfully deleted dir does not log OSError."""
127- # add a in-memory handler
128- memento = MementoHandler()
129- memento.setLevel(logging.WARNING)
130- logger.root_logger.addHandler(memento)
131-
132 local_dir = os.path.join(self.root_dir, "foo")
133 os.mkdir(local_dir)
134 self.fsm.create(local_dir, "", is_dir=True)
135@@ -1883,20 +1924,15 @@
136 self.fsm.create(local_file, "")
137 self.fsm.set_node_id(local_file, "uuid_file")
138
139+ previous = self.memento.records
140 self.fsm.delete_file(local_dir)
141
142- # remove the handler
143- logger.root_logger.removeHandler(memento)
144- self.assertFalse(memento.records, memento.records)
145+ # no logs were added
146+ self.assertEquals(previous, self.memento.records)
147
148- # FIXME: Bug #494469
149- def SKIP_test_warning_on_log_file_when_failing_delete(self):
150+ def test_warning_on_log_file_when_failing_delete(self):
151 """Test that sucessfully deleted dir does not log OSError."""
152
153- log = open(LOGFILENAME, 'r')
154- log.flush()
155- log.read() # ignore log content till now
156-
157 local_dir = os.path.join(self.root_dir, "foo")
158 self.fsm.create(local_dir, "", is_dir=True)
159 self.fsm.set_node_id(local_dir, "uuid")
160@@ -1904,11 +1940,11 @@
161 # local_dir does not exist on the file system
162 self.fsm.delete_file(local_dir)
163
164- log.flush()
165- log_content = log.read()
166- log.close()
167- log_present = 'OSError [Errno 2] No such file or directory' in log_content
168- self.assertTrue(log_present)
169+ log_msg = 'OSError [Errno 2] No such file or directory'
170+ warnings = [record.message for record in self.memento.records
171+ if record.levelname == 'WARNING']
172+ self.assertTrue(len(warnings) == 1)
173+ self.assertTrue(log_msg in warnings[0])
174
175 def test_move_dir_to_conflict(self):
176 """Test that the conflict to a dir removes children metadata."""
177
178=== modified file 'tests/syncdaemon/test_states.py'
179--- tests/syncdaemon/test_states.py 2010-03-11 15:29:45 +0000
180+++ tests/syncdaemon/test_states.py 2010-03-15 23:15:33 +0000
181@@ -21,6 +21,7 @@
182 from twisted.internet import defer, reactor
183 from twisted.trial.unittest import TestCase as TwistedTestCase
184
185+from contrib.testing.testcase import FakeLogger
186 from ubuntuone.syncdaemon.states import (
187 StateManager, ConnectionManager, QueueManager, Node
188 )
189@@ -121,27 +122,6 @@
190 return object.__getattribute__(self, name)
191
192
193-
194-class FakeLogger(object):
195- """Helper logging class."""
196- def __init__(self):
197- self.logged = dict(debug=[], warning=[])
198-
199- def _log(self, log, txt, args):
200- """Really logs."""
201- if args:
202- txt = txt % args
203- log.append(txt)
204-
205- def warning(self, txt, *args):
206- """WARNING logs."""
207- self._log(self.logged['warning'], txt, args)
208-
209- def debug(self, txt, *args):
210- """DEBUG logs."""
211- self._log(self.logged['debug'], txt, args)
212-
213-
214 class Base(TwistedTestCase):
215 """Base class for state tests."""
216 def setUp(self):
217
218=== modified file 'ubuntuone/syncdaemon/filesystem_manager.py'
219--- ubuntuone/syncdaemon/filesystem_manager.py 2010-02-11 19:13:30 +0000
220+++ ubuntuone/syncdaemon/filesystem_manager.py 2010-03-15 23:15:33 +0000
221@@ -682,22 +682,28 @@
222 Notifications are disabled for every sub-path within path.
223
224 """
225- dir_deleted = False
226+ # check metadata to see if any node in LOCAL
227 subtree = self.get_paths_starting_with(path, include_base=False)
228-
229 for p, is_dir in subtree:
230 if self.changed(path=p) == self.CHANGED_LOCAL:
231- break
232- else:
233- # no local modification in the path tree
234- for p, is_dir in subtree:
235- filter_name = is_dir and "FS_DIR_DELETE" or "FS_FILE_DELETE"
236- self.eq.add_to_mute_filter(filter_name, p)
237- self.delete_metadata(p)
238- shutil.rmtree(path)
239- dir_deleted = True
240-
241- return dir_deleted
242+ logger("Conflicting dir on remove because %r is local", p)
243+ return False
244+
245+ # check disk searching for previous conflicts
246+ for (dirpath, dirnames, filenames) in os.walk(path):
247+ for fname in filenames + dirnames:
248+ if fname.endswith(self.CONFLICT_SUFFIX):
249+ logger("Conflicting dir on remove because of previous "
250+ "conflict on: %r", os.path.join(dirpath, fname))
251+ return False
252+
253+ # all green
254+ for p, is_dir in subtree:
255+ filter_name = is_dir and "FS_DIR_DELETE" or "FS_FILE_DELETE"
256+ self.eq.add_to_mute_filter(filter_name, p)
257+ self.delete_metadata(p)
258+ shutil.rmtree(path)
259+ return True
260
261 def delete_file(self, path):
262 """Deletes a file/dir and the metadata."""

Subscribers

People subscribed via source and target branches