Merge lp:~vila/bzr/lockable-config-files into lp:bzr

Proposed by Vincent Ladeuil
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
Reviewer Review Type Date Requested Status
John A Meinel Approve
Review via email: mp+33424@code.launchpad.net

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).

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) 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 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.

review: Approve
Revision history for this message
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(str_or_unicode).

>
> 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.

Revision history for this message
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.

Revision history for this message
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(u'unicode-string')" would also look wrong in practice.

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkx74ZcACgkQJdeBCYSNAAOwMwCgirTL4Dkt7N6ycqZCALusZVcy
QDIAn3eX0ZcLYvrVqinDIDnxmSt+8Eym
=/eoB
-----END PGP SIGNATURE-----

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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