Merge lp:~vila/bzr/lockable-config-files into lp:bzr
- lockable-config-files
- Merge into bzr.dev
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | John A Meinel | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 5395 | ||||
Proposed branch: | lp:~vila/bzr/lockable-config-files | ||||
Merge into: | lp:bzr | ||||
Prerequisite: | lp:~vila/bzr/simplify-test-config-building | ||||
Diff against target: |
864 lines (+375/-67) 8 files modified
NEWS (+14/-6) bzrlib/builtins.py (+27/-14) bzrlib/config.py (+105/-24) bzrlib/plugins/launchpad/test_account.py (+1/-1) bzrlib/tests/blackbox/test_break_lock.py (+24/-1) bzrlib/tests/test_commands.py (+1/-1) bzrlib/tests/test_config.py (+202/-19) bzrlib/tests/test_smtp_connection.py (+1/-1) |
||||
To merge this branch: | bzr merge lp:~vila/bzr/lockable-config-files | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John A Meinel | Approve | ||
Review via email: mp+33424@code.launchpad.net |
Commit message
Description of the change
This is the core thread of the loom regarding config files.
This fixes bug #525571 by using a real lock (the previous fix was reducing
the racing window by using a atomic file).
Vincent Ladeuil (vila) wrote : | # |
> 305 + @classmethod
> 306 + def from_bytes(cls, unicode_bytes):
>
> It seems a little weird to call it 'unicode_bytes' and to have the function be
> 'from_bytes'. What about 'from_unicode(cls, unicode_content)' ?
I just went with what spiv proposed.
I changed it to from_string(
>
> I also thought that 'lock_write' would take care of reload(), so that callers
> couldn't actually get it wrong. (reload whenever the lock transitions from 0
> => 1 lockers.)
Hmm, that may be worth considering, but it's outside the scope of this bug.
I've not change the over data model here and forcing the call to reload
may block some use cases. What if I want to inspect the values already known
by the config before reloading from disk (which must happen inside the lock
scope) ?
The constraint to call reload() (which is new, like LockableConfig) shouldn't
be a such burden for new adopters and could even help them understand what
is really happening here (also note that I only needed to add *3* calls and
that reload() usage is more than hinted in LockableConfig docstring).
> This seems ok enough to land, but it also seems like it could be improved.
What can't ? But since this is my third submission for this bug, I think
I've spend enough time on it ;) I intend to keep working in the config area
anyway.
Andrew Bennetts (spiv) wrote : | # |
Vincent Ladeuil wrote:
> > 305 + @classmethod
> > 306 + def from_bytes(cls, unicode_bytes):
> >
> > It seems a little weird to call it 'unicode_bytes' and to have the function be
> > 'from_bytes'. What about 'from_unicode(cls, unicode_content)' ?
>
> I just went with what spiv proposed.
IIRC, I proposed (or meant to propose) calling that “utf8_bytes”.
-Andrew.
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 8/29/2010 7:49 PM, Andrew Bennetts wrote:
> Vincent Ladeuil wrote:
>>> 305 + @classmethod
>>> 306 + def from_bytes(cls, unicode_bytes):
>>>
>>> It seems a little weird to call it 'unicode_bytes' and to have the function be
>>> 'from_bytes'. What about 'from_unicode(cls, unicode_content)' ?
>>
>> I just went with what spiv proposed.
>
> IIRC, I proposed (or meant to propose) calling that “utf8_bytes”.
>
> -Andrew.
>
Right, so my big concern is calling something 'unicode' when it is a
bytes string, or calling it 'bytes' when it is a unicode string. And
calling it 'unicode_bytes' always gets it wrong in one way or another :).
If it can be either than calling the parameter "unicode_or_bytes" though
doing "from_bytes(
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAkx
QDIAn3eX0ZcLYvr
=/eoB
-----END PGP SIGNATURE-----
Preview Diff
1 | === modified file 'NEWS' |
2 | --- NEWS 2010-08-29 18:07:45 +0000 |
3 | +++ NEWS 2010-08-29 18:07:46 +0000 |
4 | @@ -59,6 +59,15 @@ |
5 | New Features |
6 | ************ |
7 | |
8 | +* ``bzr break-lock --config [location]`` can now break config files |
9 | + locks. (Vincent Ladeuil, #525571) |
10 | + |
11 | +* ``bzrlib.config.LockableConfig`` is a base class for config files that |
12 | + needs to be protected against multiple writers. All methods that |
13 | + change a configuration variable value must be decorated with |
14 | + @needs_write_lock (set_option() for example). |
15 | + (Vincent Ladeuil, #525571) |
16 | + |
17 | * The ``lp:`` prefix will now use your known username (from |
18 | ``bzr launchpad-login``) to expand ``~`` to your username. For example: |
19 | ``bzr launchpad-login user && bzr push lp:~/project/branch`` will now |
20 | @@ -87,6 +96,9 @@ |
21 | * CommitBuilder now uses the committer instead of _config.username to generate |
22 | the revision-id. (Aaron Bentley, #614404) |
23 | |
24 | +* Configuration files in ``${BZR_HOME}`` are now protected against |
25 | + concurrent writers by using a lock. (Vincent Ladeuil, #525571) |
26 | + |
27 | * Cope with Microsoft FTP Server and VSFTPd that return reply '250 |
28 | Directory created' when mkdir succeeds. (Martin Pool, #224373) |
29 | |
30 | @@ -167,8 +179,8 @@ |
31 | API Changes |
32 | *********** |
33 | |
34 | -* Configuration files should now use the ``from_bytes`` constructor the |
35 | - rather than the ``file`` parameter of the ``_get_parser`` method. The |
36 | +* ``IniBaseConfig`` objects should now use the ``from_string`` constructor |
37 | + the rather than the ``file`` parameter of the ``_get_parser`` method. The |
38 | later has been deprecated. (Vincent Ladeuil) |
39 | |
40 | * InventoryEntry instances now raise AttributeError if you try to assign |
41 | @@ -183,10 +195,6 @@ |
42 | * InventoryEntry objects no longer have ``_put_in_tar`` or |
43 | ``_put_on_disk`` methods. (Andrew Bennetts) |
44 | |
45 | -* The ``configIniBaseConfig`` constructor now accepts a ``_content`` |
46 | - private parameter for tests that want to provide a config file content. |
47 | - (Vincent Ladeuil) |
48 | - |
49 | * The ``get_filename`` parameter in the ``config.IniBaseConfig`` |
50 | constructor has been deprecated, use the ``file_name`` parameter instead. |
51 | (Vincent Ladeuil) |
52 | |
53 | === modified file 'bzrlib/builtins.py' |
54 | --- bzrlib/builtins.py 2010-08-20 09:39:20 +0000 |
55 | +++ bzrlib/builtins.py 2010-08-29 18:07:46 +0000 |
56 | @@ -32,7 +32,7 @@ |
57 | bzrdir, |
58 | directory_service, |
59 | delta, |
60 | - config, |
61 | + config as _mod_config, |
62 | errors, |
63 | globbing, |
64 | hooks, |
65 | @@ -3322,7 +3322,7 @@ |
66 | try: |
67 | c = Branch.open_containing(u'.')[0].get_config() |
68 | except errors.NotBranchError: |
69 | - c = config.GlobalConfig() |
70 | + c = _mod_config.GlobalConfig() |
71 | else: |
72 | c = Branch.open(directory).get_config() |
73 | if email: |
74 | @@ -3333,7 +3333,7 @@ |
75 | |
76 | # display a warning if an email address isn't included in the given name. |
77 | try: |
78 | - config.extract_email_address(name) |
79 | + _mod_config.extract_email_address(name) |
80 | except errors.NoEmailInUsername, e: |
81 | warning('"%s" does not seem to contain an email address. ' |
82 | 'This is allowed, but not recommended.', name) |
83 | @@ -3345,7 +3345,7 @@ |
84 | else: |
85 | c = Branch.open(directory).get_config() |
86 | else: |
87 | - c = config.GlobalConfig() |
88 | + c = _mod_config.GlobalConfig() |
89 | c.set_user_option('email', name) |
90 | |
91 | |
92 | @@ -3418,13 +3418,13 @@ |
93 | 'bzr alias --remove expects an alias to remove.') |
94 | # If alias is not found, print something like: |
95 | # unalias: foo: not found |
96 | - c = config.GlobalConfig() |
97 | + c = _mod_config.GlobalConfig() |
98 | c.unset_alias(alias_name) |
99 | |
100 | @display_command |
101 | def print_aliases(self): |
102 | """Print out the defined aliases in a similar format to bash.""" |
103 | - aliases = config.GlobalConfig().get_aliases() |
104 | + aliases = _mod_config.GlobalConfig().get_aliases() |
105 | for key, value in sorted(aliases.iteritems()): |
106 | self.outf.write('bzr alias %s="%s"\n' % (key, value)) |
107 | |
108 | @@ -3440,7 +3440,7 @@ |
109 | |
110 | def set_alias(self, alias_name, alias_command): |
111 | """Save the alias in the global config.""" |
112 | - c = config.GlobalConfig() |
113 | + c = _mod_config.GlobalConfig() |
114 | c.set_alias(alias_name, alias_command) |
115 | |
116 | |
117 | @@ -4807,7 +4807,10 @@ |
118 | |
119 | |
120 | class cmd_break_lock(Command): |
121 | - __doc__ = """Break a dead lock on a repository, branch or working directory. |
122 | + __doc__ = """Break a dead lock. |
123 | + |
124 | + This command breaks a lock on a repository, branch, working directory or |
125 | + config file. |
126 | |
127 | CAUTION: Locks should only be broken when you are sure that the process |
128 | holding the lock has been stopped. |
129 | @@ -4818,17 +4821,27 @@ |
130 | :Examples: |
131 | bzr break-lock |
132 | bzr break-lock bzr+ssh://example.com/bzr/foo |
133 | + bzr break-lock --conf ~/.bazaar |
134 | """ |
135 | + |
136 | takes_args = ['location?'] |
137 | + takes_options = [ |
138 | + Option('config', |
139 | + help='LOCATION is the directory where the config lock is.'), |
140 | + ] |
141 | |
142 | - def run(self, location=None, show=False): |
143 | + def run(self, location=None, config=False): |
144 | if location is None: |
145 | location = u'.' |
146 | - control, relpath = bzrdir.BzrDir.open_containing(location) |
147 | - try: |
148 | - control.break_lock() |
149 | - except NotImplementedError: |
150 | - pass |
151 | + if config: |
152 | + conf = _mod_config.LockableConfig(file_name=location) |
153 | + conf.break_lock() |
154 | + else: |
155 | + control, relpath = bzrdir.BzrDir.open_containing(location) |
156 | + try: |
157 | + control.break_lock() |
158 | + except NotImplementedError: |
159 | + pass |
160 | |
161 | |
162 | class cmd_wait_until_signalled(Command): |
163 | |
164 | === modified file 'bzrlib/config.py' |
165 | --- bzrlib/config.py 2010-08-29 18:07:45 +0000 |
166 | +++ bzrlib/config.py 2010-08-29 18:07:46 +0000 |
167 | @@ -65,6 +65,7 @@ |
168 | import os |
169 | import sys |
170 | |
171 | +from bzrlib.decorators import needs_write_lock |
172 | from bzrlib.lazy_import import lazy_import |
173 | lazy_import(globals(), """ |
174 | import errno |
175 | @@ -77,11 +78,13 @@ |
176 | atomicfile, |
177 | debug, |
178 | errors, |
179 | + lockdir, |
180 | mail_client, |
181 | osutils, |
182 | registry, |
183 | symbol_versioning, |
184 | trace, |
185 | + transport, |
186 | ui, |
187 | urlutils, |
188 | win32utils, |
189 | @@ -359,17 +362,14 @@ |
190 | :param file_name: The configuration file path. |
191 | """ |
192 | super(IniBasedConfig, self).__init__() |
193 | + self.file_name = file_name |
194 | if symbol_versioning.deprecated_passed(get_filename): |
195 | symbol_versioning.warn( |
196 | 'IniBasedConfig.__init__(get_filename) was deprecated in 2.3.' |
197 | ' Use file_name instead.', |
198 | DeprecationWarning, |
199 | stacklevel=2) |
200 | - if get_filename is None: |
201 | - # Tests use in-memory config files that doesn't need to be |
202 | - # saved on disk |
203 | - self.file_name = None |
204 | - else: |
205 | + if get_filename is not None: |
206 | self.file_name = get_filename() |
207 | else: |
208 | self.file_name = file_name |
209 | @@ -377,16 +377,21 @@ |
210 | self._parser = None |
211 | |
212 | @classmethod |
213 | - def from_bytes(cls, unicode_bytes): |
214 | - """Create a config object from bytes. |
215 | + def from_string(cls, str_or_unicode, file_name=None): |
216 | + """Create a config object from a string. |
217 | |
218 | - :param unicode_bytes: A string representing the file content. This will |
219 | + :param str_or_unicode: A string representing the file content. This will |
220 | be utf-8 encoded. |
221 | + |
222 | + :param file_name: The configuration file path. |
223 | """ |
224 | - conf = cls() |
225 | - conf._content = StringIO(unicode_bytes.encode('utf-8')) |
226 | + conf = cls(file_name=file_name) |
227 | + conf._create_from_string(str_or_unicode) |
228 | return conf |
229 | |
230 | + def _create_from_string(self, str_or_unicode): |
231 | + self._content = StringIO(str_or_unicode.encode('utf-8')) |
232 | + |
233 | def _get_parser(self, file=symbol_versioning.DEPRECATED_PARAMETER): |
234 | if self._parser is not None: |
235 | return self._parser |
236 | @@ -406,8 +411,17 @@ |
237 | self._parser = ConfigObj(co_input, encoding='utf-8') |
238 | except configobj.ConfigObjError, e: |
239 | raise errors.ParseConfigError(e.errors, e.config.filename) |
240 | + # Make sure self.reload() will use the right file name |
241 | + self._parser.filename = self.file_name |
242 | return self._parser |
243 | |
244 | + def reload(self): |
245 | + """Reload the config file from disk.""" |
246 | + if self.file_name is None: |
247 | + raise AssertionError('We need a file name to reload the config') |
248 | + if self._parser is not None: |
249 | + self._parser.reload() |
250 | + |
251 | def _get_matching_sections(self): |
252 | """Return an ordered list of (section_name, extra_path) pairs. |
253 | |
254 | @@ -519,6 +533,8 @@ |
255 | def _write_config_file(self): |
256 | if self.file_name is None: |
257 | raise AssertionError('We cannot save, self.file_name is None') |
258 | + conf_dir = os.path.dirname(self.file_name) |
259 | + ensure_config_dir_exists(conf_dir) |
260 | atomic_file = atomicfile.AtomicFile(self.file_name) |
261 | self._get_parser().write(atomic_file) |
262 | atomic_file.commit() |
263 | @@ -526,15 +542,82 @@ |
264 | osutils.copy_ownership_from_path(self.file_name) |
265 | |
266 | |
267 | -class GlobalConfig(IniBasedConfig): |
268 | +class LockableConfig(IniBasedConfig): |
269 | + """A configuration needing explicit locking for access. |
270 | + |
271 | + If several processes try to write the config file, the accesses need to be |
272 | + serialized. |
273 | + |
274 | + Daughter classes should decorate all methods that update a config with the |
275 | + ``@needs_write_lock`` decorator (they call, directly or indirectly, the |
276 | + ``_write_config_file()`` method. These methods (typically ``set_option()`` |
277 | + and variants must reload the config file from disk before calling |
278 | + ``_write_config_file()``), this can be achieved by calling the |
279 | + ``self.reload()`` method. Note that the lock scope should cover both the |
280 | + reading and the writing of the config file which is why the decorator can't |
281 | + be applied to ``_write_config_file()`` only. |
282 | + |
283 | + This should be enough to implement the following logic: |
284 | + - lock for exclusive write access, |
285 | + - reload the config file from disk, |
286 | + - set the new value |
287 | + - unlock |
288 | + |
289 | + This logic guarantees that a writer can update a value without erasing an |
290 | + update made by another writer. |
291 | + """ |
292 | + |
293 | + lock_name = 'lock' |
294 | + |
295 | + def __init__(self, file_name): |
296 | + super(LockableConfig, self).__init__(file_name=file_name) |
297 | + self.dir = osutils.dirname(osutils.safe_unicode(self.file_name)) |
298 | + self.transport = transport.get_transport(self.dir) |
299 | + self._lock = lockdir.LockDir(self.transport, 'lock') |
300 | + |
301 | + def lock_write(self, token=None): |
302 | + """Takes a write lock in the directory containing the config file. |
303 | + |
304 | + If the directory doesn't exist it is created. |
305 | + """ |
306 | + ensure_config_dir_exists(self.dir) |
307 | + return self._lock.lock_write(token) |
308 | + |
309 | + def unlock(self): |
310 | + self._lock.unlock() |
311 | + |
312 | + def break_lock(self): |
313 | + self._lock.break_lock() |
314 | + |
315 | + def _write_config_file(self): |
316 | + if self._lock is None or not self._lock.is_held: |
317 | + # NB: if the following exception is raised it probably means a |
318 | + # missing @needs_write_lock decorator on one of the callers. |
319 | + raise errors.ObjectNotLocked(self) |
320 | + super(LockableConfig, self)._write_config_file() |
321 | + |
322 | + |
323 | +class GlobalConfig(LockableConfig): |
324 | """The configuration that should be used for a specific location.""" |
325 | |
326 | def __init__(self): |
327 | super(GlobalConfig, self).__init__(file_name=config_filename()) |
328 | |
329 | + @classmethod |
330 | + def from_string(cls, str_or_unicode): |
331 | + """Create a config object from a string. |
332 | + |
333 | + :param str_or_unicode: A string representing the file content. This |
334 | + will be utf-8 encoded. |
335 | + """ |
336 | + conf = cls() |
337 | + conf._create_from_string(str_or_unicode) |
338 | + return conf |
339 | + |
340 | def get_editor(self): |
341 | return self._get_user_option('editor') |
342 | |
343 | + @needs_write_lock |
344 | def set_user_option(self, option, value): |
345 | """Save option and its value in the configuration.""" |
346 | self._set_option(option, value, 'DEFAULT') |
347 | @@ -546,12 +629,15 @@ |
348 | else: |
349 | return {} |
350 | |
351 | + @needs_write_lock |
352 | def set_alias(self, alias_name, alias_command): |
353 | """Save the alias in the configuration.""" |
354 | self._set_option(alias_name, alias_command, 'ALIASES') |
355 | |
356 | + @needs_write_lock |
357 | def unset_alias(self, alias_name): |
358 | """Unset an existing alias.""" |
359 | + self.reload() |
360 | aliases = self._get_parser().get('ALIASES') |
361 | if not aliases or alias_name not in aliases: |
362 | raise errors.NoSuchAlias(alias_name) |
363 | @@ -559,15 +645,12 @@ |
364 | self._write_config_file() |
365 | |
366 | def _set_option(self, option, value, section): |
367 | - # FIXME: RBC 20051029 This should refresh the parser and also take a |
368 | - # file lock on bazaar.conf. |
369 | - conf_dir = os.path.dirname(self.file_name) |
370 | - ensure_config_dir_exists(conf_dir) |
371 | + self.reload() |
372 | self._get_parser().setdefault(section, {})[option] = value |
373 | self._write_config_file() |
374 | |
375 | |
376 | -class LocationConfig(IniBasedConfig): |
377 | +class LocationConfig(LockableConfig): |
378 | """A configuration object that gives the policy for a location.""" |
379 | |
380 | def __init__(self, location): |
381 | @@ -581,16 +664,16 @@ |
382 | self.location = location |
383 | |
384 | @classmethod |
385 | - def from_bytes(cls, unicode_bytes, location): |
386 | - """Create a config object from bytes. |
387 | + def from_string(cls, str_or_unicode, location): |
388 | + """Create a config object from s string. |
389 | |
390 | - :param unicode_bytes: A string representing the file content. This will |
391 | + :param str_or_unicode: A string representing the file content. This will |
392 | be utf-8 encoded. |
393 | |
394 | :param location: The location url to filter the configuration. |
395 | """ |
396 | conf = cls(location) |
397 | - conf._content = StringIO(unicode_bytes.encode('utf-8')) |
398 | + conf._create_from_string(str_or_unicode) |
399 | return conf |
400 | |
401 | def _get_matching_sections(self): |
402 | @@ -686,6 +769,7 @@ |
403 | if policy_key in self._get_parser()[section]: |
404 | del self._get_parser()[section][policy_key] |
405 | |
406 | + @needs_write_lock |
407 | def set_user_option(self, option, value, store=STORE_LOCATION): |
408 | """Save option and its value in the configuration.""" |
409 | if store not in [STORE_LOCATION, |
410 | @@ -693,10 +777,7 @@ |
411 | STORE_LOCATION_APPENDPATH]: |
412 | raise ValueError('bad storage policy %r for %r' % |
413 | (store, option)) |
414 | - # FIXME: RBC 20051029 This should refresh the parser and also take a |
415 | - # file lock on locations.conf. |
416 | - conf_dir = os.path.dirname(self.file_name) |
417 | - ensure_config_dir_exists(conf_dir) |
418 | + self.reload() |
419 | location = self.location |
420 | if location.endswith('/'): |
421 | location = location[:-1] |
422 | |
423 | === modified file 'bzrlib/plugins/launchpad/test_account.py' |
424 | --- bzrlib/plugins/launchpad/test_account.py 2010-08-29 18:07:45 +0000 |
425 | +++ bzrlib/plugins/launchpad/test_account.py 2010-08-29 18:07:46 +0000 |
426 | @@ -26,7 +26,7 @@ |
427 | class LaunchpadAccountTests(TestCaseInTempDir): |
428 | |
429 | def setup_config(self, text): |
430 | - my_config = config.GlobalConfig.from_bytes(text) |
431 | + my_config = config.GlobalConfig.from_string(text) |
432 | return my_config |
433 | |
434 | def test_get_lp_login_unconfigured(self): |
435 | |
436 | === modified file 'bzrlib/tests/blackbox/test_break_lock.py' |
437 | --- bzrlib/tests/blackbox/test_break_lock.py 2010-07-21 13:32:27 +0000 |
438 | +++ bzrlib/tests/blackbox/test_break_lock.py 2010-08-29 18:07:46 +0000 |
439 | @@ -22,8 +22,10 @@ |
440 | from bzrlib import ( |
441 | branch, |
442 | bzrdir, |
443 | + config, |
444 | errors, |
445 | lockdir, |
446 | + osutils, |
447 | tests, |
448 | ) |
449 | |
450 | @@ -73,7 +75,7 @@ |
451 | # however, we dont test breaking the working tree because we |
452 | # cannot accurately do so right now: the dirstate lock is held |
453 | # by an os lock, and we need to spawn a separate process to lock it |
454 | - # thne kill -9 it. |
455 | + # then kill -9 it. |
456 | # sketch of test: |
457 | # lock most of the dir: |
458 | self.wt.branch.lock_write() |
459 | @@ -101,3 +103,24 @@ |
460 | out, err = self.run_bzr('break-lock foo') |
461 | self.assertEqual('', out) |
462 | self.assertEqual('', err) |
463 | + |
464 | +class TestConfigBreakLock(tests.TestCaseWithTransport): |
465 | + |
466 | + def setUp(self): |
467 | + super(TestConfigBreakLock, self).setUp() |
468 | + self.config_file_name = './my.conf' |
469 | + self.build_tree_contents([(self.config_file_name, |
470 | + '[DEFAULT]\none=1\n')]) |
471 | + self.config = config.LockableConfig(file_name=self.config_file_name) |
472 | + self.config.lock_write() |
473 | + |
474 | + def test_create_pending_lock(self): |
475 | + self.addCleanup(self.config.unlock) |
476 | + self.assertTrue(self.config._lock.is_held) |
477 | + |
478 | + def test_break_lock(self): |
479 | + self.run_bzr('break-lock --config %s' |
480 | + % osutils.dirname(self.config_file_name), |
481 | + stdin="y\n") |
482 | + self.assertRaises(errors.LockBroken, self.config.unlock) |
483 | + |
484 | |
485 | === modified file 'bzrlib/tests/test_commands.py' |
486 | --- bzrlib/tests/test_commands.py 2010-08-29 18:07:45 +0000 |
487 | +++ bzrlib/tests/test_commands.py 2010-08-29 18:07:46 +0000 |
488 | @@ -95,7 +95,7 @@ |
489 | class TestGetAlias(tests.TestCase): |
490 | |
491 | def _get_config(self, config_text): |
492 | - my_config = config.GlobalConfig.from_bytes(config_text) |
493 | + my_config = config.GlobalConfig.from_string(config_text) |
494 | return my_config |
495 | |
496 | def test_simple(self): |
497 | |
498 | === modified file 'bzrlib/tests/test_config.py' |
499 | --- bzrlib/tests/test_config.py 2010-08-29 18:07:45 +0000 |
500 | +++ bzrlib/tests/test_config.py 2010-08-29 18:07:46 +0000 |
501 | @@ -19,6 +19,7 @@ |
502 | from cStringIO import StringIO |
503 | import os |
504 | import sys |
505 | +import threading |
506 | |
507 | #import bzrlib specific imports here |
508 | from bzrlib import ( |
509 | @@ -39,6 +40,30 @@ |
510 | from bzrlib.util.configobj import configobj |
511 | |
512 | |
513 | +def lockable_config_scenarios(): |
514 | + return [ |
515 | + ('global', |
516 | + {'config_class': config.GlobalConfig, |
517 | + 'config_args': [], |
518 | + 'config_section': 'DEFAULT'}), |
519 | + ('locations', |
520 | + {'config_class': config.LocationConfig, |
521 | + 'config_args': ['.'], |
522 | + 'config_section': '.'}),] |
523 | + |
524 | + |
525 | +def load_tests(standard_tests, module, loader): |
526 | + suite = loader.suiteClass() |
527 | + |
528 | + lc_tests, remaining_tests = tests.split_suite_by_condition( |
529 | + standard_tests, tests.condition_isinstance(( |
530 | + TestLockableConfig, |
531 | + ))) |
532 | + tests.multiply_tests(lc_tests, lockable_config_scenarios(), suite) |
533 | + suite.addTest(remaining_tests) |
534 | + return suite |
535 | + |
536 | + |
537 | sample_long_alias="log -r-15..-1 --line" |
538 | sample_config_text = u""" |
539 | [DEFAULT] |
540 | @@ -130,6 +155,9 @@ |
541 | self._calls.append(('keys',)) |
542 | return [] |
543 | |
544 | + def reload(self): |
545 | + self._calls.append(('reload',)) |
546 | + |
547 | def write(self, arg): |
548 | self._calls.append(('write',)) |
549 | |
550 | @@ -367,7 +395,7 @@ |
551 | class TestIniConfig(tests.TestCaseInTempDir): |
552 | |
553 | def make_config_parser(self, s): |
554 | - conf = config.IniBasedConfig.from_bytes(s) |
555 | + conf = config.IniBasedConfig.from_string(s) |
556 | return conf, conf._get_parser() |
557 | |
558 | |
559 | @@ -377,11 +405,11 @@ |
560 | my_config = config.IniBasedConfig() |
561 | |
562 | def test_from_fp(self): |
563 | - my_config = config.IniBasedConfig.from_bytes(sample_config_text) |
564 | + my_config = config.IniBasedConfig.from_string(sample_config_text) |
565 | self.assertIsInstance(my_config._get_parser(), configobj.ConfigObj) |
566 | |
567 | def test_cached(self): |
568 | - my_config = config.IniBasedConfig.from_bytes(sample_config_text) |
569 | + my_config = config.IniBasedConfig.from_string(sample_config_text) |
570 | parser = my_config._get_parser() |
571 | self.failUnless(my_config._get_parser() is parser) |
572 | |
573 | @@ -389,14 +417,13 @@ |
574 | self.path, self.uid, self.gid = path, uid, gid |
575 | |
576 | def test_ini_config_ownership(self): |
577 | - """Ensure that chown is happening during _write_config_file. |
578 | - """ |
579 | + """Ensure that chown is happening during _write_config_file""" |
580 | self.requireFeature(features.chown_feature) |
581 | self.overrideAttr(os, 'chown', self._dummy_chown) |
582 | self.path = self.uid = self.gid = None |
583 | - conf = config.IniBasedConfig(file_name='foo.conf') |
584 | + conf = config.IniBasedConfig(file_name='./foo.conf') |
585 | conf._write_config_file() |
586 | - self.assertEquals(self.path, 'foo.conf') |
587 | + self.assertEquals(self.path, './foo.conf') |
588 | self.assertTrue(isinstance(self.uid, int)) |
589 | self.assertTrue(isinstance(self.gid, int)) |
590 | |
591 | @@ -409,7 +436,7 @@ |
592 | |
593 | def test_get_parser_file_parameter_is_deprecated_(self): |
594 | config_file = StringIO(sample_config_text.encode('utf-8')) |
595 | - conf = config.IniBasedConfig.from_bytes(sample_config_text) |
596 | + conf = config.IniBasedConfig.from_string(sample_config_text) |
597 | conf = self.callDeprecated([ |
598 | 'IniBasedConfig._get_parser(file=xxx) was deprecated in 2.3.' |
599 | ' Use IniBasedConfig(_content=xxx) instead.'], |
600 | @@ -420,6 +447,153 @@ |
601 | self.assertRaises(AssertionError, conf._write_config_file) |
602 | |
603 | |
604 | +class TestIniBaseConfigOnDisk(tests.TestCaseInTempDir): |
605 | + |
606 | + def test_cannot_reload_without_name(self): |
607 | + conf = config.IniBasedConfig.from_string(sample_config_text) |
608 | + self.assertRaises(AssertionError, conf.reload) |
609 | + |
610 | + def test_reload_see_new_value(self): |
611 | + c1 = config.IniBasedConfig.from_string('editor=vim\n', |
612 | + file_name='./test/conf') |
613 | + c1._write_config_file() |
614 | + c2 = config.IniBasedConfig.from_string('editor=emacs\n', |
615 | + file_name='./test/conf') |
616 | + c2._write_config_file() |
617 | + self.assertEqual('vim', c1.get_user_option('editor')) |
618 | + self.assertEqual('emacs', c2.get_user_option('editor')) |
619 | + # Make sure we get the Right value |
620 | + c1.reload() |
621 | + self.assertEqual('emacs', c1.get_user_option('editor')) |
622 | + |
623 | + |
624 | +class TestLockableConfig(tests.TestCaseInTempDir): |
625 | + |
626 | + # Set by load_tests |
627 | + config_class = None |
628 | + config_args = None |
629 | + config_section = None |
630 | + |
631 | + def setUp(self): |
632 | + super(TestLockableConfig, self).setUp() |
633 | + self._content = '[%s]\none=1\ntwo=2\n' % (self.config_section,) |
634 | + self.config = self.create_config(self._content) |
635 | + |
636 | + def get_existing_config(self): |
637 | + return self.config_class(*self.config_args) |
638 | + |
639 | + def create_config(self, content): |
640 | + c = self.config_class.from_string(content, *self.config_args) |
641 | + c.lock_write() |
642 | + c._write_config_file() |
643 | + c.unlock() |
644 | + return c |
645 | + |
646 | + def test_simple_read_access(self): |
647 | + self.assertEquals('1', self.config.get_user_option('one')) |
648 | + |
649 | + def test_simple_write_access(self): |
650 | + self.config.set_user_option('one', 'one') |
651 | + self.assertEquals('one', self.config.get_user_option('one')) |
652 | + |
653 | + def test_listen_to_the_last_speaker(self): |
654 | + c1 = self.config |
655 | + c2 = self.get_existing_config() |
656 | + c1.set_user_option('one', 'ONE') |
657 | + c2.set_user_option('two', 'TWO') |
658 | + self.assertEquals('ONE', c1.get_user_option('one')) |
659 | + self.assertEquals('TWO', c2.get_user_option('two')) |
660 | + # The second update respect the first one |
661 | + self.assertEquals('ONE', c2.get_user_option('one')) |
662 | + |
663 | + def test_last_speaker_wins(self): |
664 | + # If the same config is not shared, the same variable modified twice |
665 | + # can only see a single result. |
666 | + c1 = self.config |
667 | + c2 = self.get_existing_config() |
668 | + c1.set_user_option('one', 'c1') |
669 | + c2.set_user_option('one', 'c2') |
670 | + self.assertEquals('c2', c2._get_user_option('one')) |
671 | + # The first modification is still available until another refresh |
672 | + # occur |
673 | + self.assertEquals('c1', c1._get_user_option('one')) |
674 | + c1.set_user_option('two', 'done') |
675 | + self.assertEquals('c2', c1._get_user_option('one')) |
676 | + |
677 | + def test_writes_are_serialized(self): |
678 | + c1 = self.config |
679 | + c2 = self.get_existing_config() |
680 | + |
681 | + # We spawn a thread that will pause *during* the write |
682 | + before_writing = threading.Event() |
683 | + after_writing = threading.Event() |
684 | + writing_done = threading.Event() |
685 | + c1_orig = c1._write_config_file |
686 | + def c1_write_config_file(): |
687 | + before_writing.set() |
688 | + c1_orig() |
689 | + # The lock is held we wait for the main thread to decide when to |
690 | + # continue |
691 | + after_writing.wait() |
692 | + c1._write_config_file = c1_write_config_file |
693 | + def c1_set_option(): |
694 | + c1.set_user_option('one', 'c1') |
695 | + writing_done.set() |
696 | + t1 = threading.Thread(target=c1_set_option) |
697 | + # Collect the thread after the test |
698 | + self.addCleanup(t1.join) |
699 | + # Be ready to unblock the thread if the test goes wrong |
700 | + self.addCleanup(after_writing.set) |
701 | + t1.start() |
702 | + before_writing.wait() |
703 | + self.assertTrue(c1._lock.is_held) |
704 | + self.assertRaises(errors.LockContention, |
705 | + c2.set_user_option, 'one', 'c2') |
706 | + self.assertEquals('c1', c1.get_user_option('one')) |
707 | + # Let the lock be released |
708 | + after_writing.set() |
709 | + writing_done.wait() |
710 | + c2.set_user_option('one', 'c2') |
711 | + self.assertEquals('c2', c2.get_user_option('one')) |
712 | + |
713 | + def test_read_while_writing(self): |
714 | + c1 = self.config |
715 | + # We spawn a thread that will pause *during* the write |
716 | + ready_to_write = threading.Event() |
717 | + do_writing = threading.Event() |
718 | + writing_done = threading.Event() |
719 | + c1_orig = c1._write_config_file |
720 | + def c1_write_config_file(): |
721 | + ready_to_write.set() |
722 | + # The lock is held we wait for the main thread to decide when to |
723 | + # continue |
724 | + do_writing.wait() |
725 | + c1_orig() |
726 | + writing_done.set() |
727 | + c1._write_config_file = c1_write_config_file |
728 | + def c1_set_option(): |
729 | + c1.set_user_option('one', 'c1') |
730 | + t1 = threading.Thread(target=c1_set_option) |
731 | + # Collect the thread after the test |
732 | + self.addCleanup(t1.join) |
733 | + # Be ready to unblock the thread if the test goes wrong |
734 | + self.addCleanup(do_writing.set) |
735 | + t1.start() |
736 | + # Ensure the thread is ready to write |
737 | + ready_to_write.wait() |
738 | + self.assertTrue(c1._lock.is_held) |
739 | + self.assertEquals('c1', c1.get_user_option('one')) |
740 | + # If we read during the write, we get the old value |
741 | + c2 = self.get_existing_config() |
742 | + self.assertEquals('1', c2.get_user_option('one')) |
743 | + # Let the writing occur and ensure it occurred |
744 | + do_writing.set() |
745 | + writing_done.wait() |
746 | + # Now we get the updated value |
747 | + c3 = self.get_existing_config() |
748 | + self.assertEquals('c1', c3.get_user_option('one')) |
749 | + |
750 | + |
751 | class TestGetUserOptionAs(TestIniConfig): |
752 | |
753 | def test_get_user_option_as_bool(self): |
754 | @@ -607,7 +781,7 @@ |
755 | class TestGlobalConfigItems(tests.TestCase): |
756 | |
757 | def test_user_id(self): |
758 | - my_config = config.GlobalConfig.from_bytes(sample_config_text) |
759 | + my_config = config.GlobalConfig.from_string(sample_config_text) |
760 | self.assertEqual(u"Erik B\u00e5gfors <erik@bagfors.nu>", |
761 | my_config._get_user_id()) |
762 | |
763 | @@ -616,11 +790,11 @@ |
764 | self.assertEqual(None, my_config._get_user_id()) |
765 | |
766 | def test_configured_editor(self): |
767 | - my_config = config.GlobalConfig.from_bytes(sample_config_text) |
768 | + my_config = config.GlobalConfig.from_string(sample_config_text) |
769 | self.assertEqual("vim", my_config.get_editor()) |
770 | |
771 | def test_signatures_always(self): |
772 | - my_config = config.GlobalConfig.from_bytes(sample_always_signatures) |
773 | + my_config = config.GlobalConfig.from_string(sample_always_signatures) |
774 | self.assertEqual(config.CHECK_NEVER, |
775 | my_config.signature_checking()) |
776 | self.assertEqual(config.SIGN_ALWAYS, |
777 | @@ -628,7 +802,7 @@ |
778 | self.assertEqual(True, my_config.signature_needed()) |
779 | |
780 | def test_signatures_if_possible(self): |
781 | - my_config = config.GlobalConfig.from_bytes(sample_maybe_signatures) |
782 | + my_config = config.GlobalConfig.from_string(sample_maybe_signatures) |
783 | self.assertEqual(config.CHECK_NEVER, |
784 | my_config.signature_checking()) |
785 | self.assertEqual(config.SIGN_WHEN_REQUIRED, |
786 | @@ -636,7 +810,7 @@ |
787 | self.assertEqual(False, my_config.signature_needed()) |
788 | |
789 | def test_signatures_ignore(self): |
790 | - my_config = config.GlobalConfig.from_bytes(sample_ignore_signatures) |
791 | + my_config = config.GlobalConfig.from_string(sample_ignore_signatures) |
792 | self.assertEqual(config.CHECK_ALWAYS, |
793 | my_config.signature_checking()) |
794 | self.assertEqual(config.SIGN_NEVER, |
795 | @@ -644,7 +818,7 @@ |
796 | self.assertEqual(False, my_config.signature_needed()) |
797 | |
798 | def _get_sample_config(self): |
799 | - my_config = config.GlobalConfig.from_bytes(sample_config_text) |
800 | + my_config = config.GlobalConfig.from_string(sample_config_text) |
801 | return my_config |
802 | |
803 | def test_gpg_signing_command(self): |
804 | @@ -993,11 +1167,15 @@ |
805 | global_config = sample_config_text |
806 | |
807 | config.ensure_config_dir_exists() |
808 | - my_global_config = config.GlobalConfig.from_bytes(global_config) |
809 | + my_global_config = config.GlobalConfig.from_string(global_config) |
810 | + my_global_config.lock_write() |
811 | my_global_config._write_config_file() |
812 | - my_location_config = config.LocationConfig.from_bytes( |
813 | + my_global_config.unlock() |
814 | + my_location_config = config.LocationConfig.from_string( |
815 | sample_branches_text, my_branch.base) |
816 | + my_location_config.lock_write() |
817 | my_location_config._write_config_file() |
818 | + my_location_config.unlock() |
819 | |
820 | my_config = config.BranchConfig(my_branch) |
821 | self.my_config = my_config |
822 | @@ -1013,7 +1191,8 @@ |
823 | 'converted to use policies.'], |
824 | self.my_config.set_user_option, |
825 | 'foo', 'bar', store=config.STORE_LOCATION) |
826 | - self.assertEqual([('__contains__', '/a/c'), |
827 | + self.assertEqual([('reload',), |
828 | + ('__contains__', '/a/c'), |
829 | ('__contains__', '/a/c/'), |
830 | ('__setitem__', '/a/c', {}), |
831 | ('__getitem__', '/a/c'), |
832 | @@ -1068,14 +1247,18 @@ |
833 | location_config=None, branch_data_config=None): |
834 | my_branch = FakeBranch(location) |
835 | if global_config is not None: |
836 | - my_global_config = config.GlobalConfig.from_bytes(global_config) |
837 | + my_global_config = config.GlobalConfig.from_string(global_config) |
838 | config.ensure_config_dir_exists() |
839 | + my_global_config.lock_write() |
840 | my_global_config._write_config_file() |
841 | + my_global_config.unlock() |
842 | if location_config is not None: |
843 | - my_location_config = config.LocationConfig.from_bytes( |
844 | + my_location_config = config.LocationConfig.from_string( |
845 | location_config, my_branch.base) |
846 | config.ensure_config_dir_exists() |
847 | + my_location_config.lock_write() |
848 | my_location_config._write_config_file() |
849 | + my_location_config.unlock() |
850 | my_config = config.BranchConfig(my_branch) |
851 | if branch_data_config is not None: |
852 | my_config.branch.control_files.files['branch.conf'] = \ |
853 | |
854 | === modified file 'bzrlib/tests/test_smtp_connection.py' |
855 | --- bzrlib/tests/test_smtp_connection.py 2010-08-29 18:07:45 +0000 |
856 | +++ bzrlib/tests/test_smtp_connection.py 2010-08-29 18:07:46 +0000 |
857 | @@ -91,7 +91,7 @@ |
858 | class TestSMTPConnection(tests.TestCaseInTempDir): |
859 | |
860 | def get_connection(self, text, smtp_factory=None): |
861 | - my_config = config.GlobalConfig.from_bytes(text) |
862 | + my_config = config.GlobalConfig.from_string(text) |
863 | return smtp_connection.SMTPConnection(my_config, |
864 | _smtp_factory=smtp_factory) |
865 |
305 + @classmethod
306 + def from_bytes(cls, unicode_bytes):
It seems a little weird to call it 'unicode_bytes' and to have the function be 'from_bytes'. What about 'from_unicode(cls, unicode_content)' ?
I also thought that 'lock_write' would take care of reload(), so that callers couldn't actually get it wrong. (reload whenever the lock transitions from 0 => 1 lockers.)
This seems ok enough to land, but it also seems like it could be improved.