Merge lp:~vila/bzr/525571-lock-bazaar-conf-files into lp:bzr

Proposed by Vincent Ladeuil
Status: Superseded
Proposed branch: lp:~vila/bzr/525571-lock-bazaar-conf-files
Merge into: lp:bzr
Diff against target: 779 lines (+360/-87) (has conflicts)
6 files modified
NEWS (+15/-0)
bzrlib/builtins.py (+20/-7)
bzrlib/config.py (+87/-22)
bzrlib/tests/blackbox/test_break_lock.py (+44/-20)
bzrlib/tests/test_commands.py (+1/-1)
bzrlib/tests/test_config.py (+193/-37)
Text conflict in NEWS
To merge this branch: bzr merge lp:~vila/bzr/525571-lock-bazaar-conf-files
Reviewer Review Type Date Requested Status
Robert Collins (community) Needs Fixing
Review via email: mp+28764@code.launchpad.net

This proposal has been superseded by a proposal from 2010-06-30.

Description of the change

This implements a write lock on LocationConfig and GlobalConfig and add support for them in break-lock
as required for bug #525571.

There is no user nor dev visible change but I'll welcome feedback from plugin authors.

There is a new bzrlib.config.LockableConfig that plugins authors using config files in ~/.bazaar may want to inherit (as LocationConfig and GlobalConfig do now).

I had to do some cleanup in the tests as modifying the model made quite a few of them fail
(congrats to test authors: failing tests are good tests ! :)

So I made a bit more cleanup than strictly necessary (during failure analysis),
my apologies to the reviewers.

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Vincent Ladeuil wrote:
> Vincent Ladeuil has proposed merging lp:~vila/bzr/525571-lock-bazaar-conf-files into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
> Related bugs:
> #525571 No locking when updating files in ~/.bazaar
> https://bugs.launchpad.net/bugs/525571
>
>
> This implements a write lock on LocationConfig and GlobalConfig and add support for them in break-lock
> as required for bug #525571.
>
> There is no user nor dev visible change but I'll welcome feedback from plugin authors.
>
> There is a new bzrlib.config.LockableConfig that plugins authors using config files in ~/.bazaar may want to inherit (as LocationConfig and GlobalConfig do now).
>
> I had to do some cleanup in the tests as modifying the model made quite a few of them fail
> (congrats to test authors: failing tests are good tests ! :)
>
> So I made a bit more cleanup than strictly necessary (during failure analysis),
> my apologies to the reviewers.
>

As a comment, without having really read the code thoroughly.

How does this handle stuff like 2 branches locking concurrently
locations.conf. I don't know how often we do it internally, though.

I think lots of filesystem locks on the bazaar directory could adversely
affect performance on Windows. IME locking isn't too expensive if you do
it 1 or 2 times. But if you lock and unlock on every attribute that gets
set, then it probably starts to be an issue.

On a Windows host:
  $ TIMEIT -s "b = Branch.open('.')" "b.lock_write(); b.unlock()"
  10.5msec

On an Ubuntu VM on the same machine:
  $ TIMEIT -s "b = Branch.open('.')" "b.lock_write(); b.unlock()"
  1.55msec

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

iEYEARECAAYFAkwqMNkACgkQJdeBCYSNAAO7WACfYZOte5LfqA4Ro4J6U/3ZA2Cf
ZhkAoLy3d0+tQjcgx047AYI0sJMiZlfY
=mMJj
-----END PGP SIGNATURE-----

Revision history for this message
Robert Collins (lifeless) wrote :

07:04 -!- guilhembi
[~<email address hidden>] has quit
[Quit: Client exiting]
07:11 < lifeless> vila:
07:11 < lifeless> + __doc__ = """\
07:11 < lifeless> + Break a dead lock on a repository, branch,
working directory or config file.
07:11 < lifeless> I'd prefer to see that spelt as
07:11 < lifeless> __doc__ = \
07:11 < lifeless> """Break...
07:12 < lifeless> because having to actually think about what you were
escaping hurt my brain

Revision history for this message
Robert Collins (lifeless) wrote :

Meta: This seems like a case where two threads would be nice:
a) fix the tests that are currently a bit gratuitous.
b) make the behaviour change

Revision history for this message
Robert Collins (lifeless) wrote :

Ok, actual review stuff:
 the docstring layout is wrong, please nuke the \.

We should check for branches first, not config files, because branch locks are the common case and break-lock doesn't need to be slow.

This change is suspicous:

152 def _write_config_file(self):
153 - f = file(self._get_filename(), "wb")
154 + fname = self._get_filename()
155 + conf_dir = os.path.dirname(fname)
156 + ensure_config_dir_exists(conf_dir)
157 + f = file(fname, "wb")
158 try:
159 - osutils.copy_ownership_from_path(f.name)
160 + osutils.copy_ownership_from_path(fname)
161 self._get_parser().write(f)
162 finally:
163 f.close()
164

It appears to be adding a new stat/mkdir check, at the wrong layer.

missing VWS:

172 + """
173 + lock_name = 'lock'

Ditto here:

181 + def lock_write(self, token=None):
182 + if self.lock is None:
183 + ensure_config_dir_exists(self.dir)
184 + self._lock = lockdir.LockDir(self.transport, self.lock_name)
185 + self._lock.lock_write(token)
186 + return lock.LogicalLockResult(self.unlock)

If the dir doesn't exist, something is wrong - we really shouldn't have gotten this far without it, should we?

Docstrings!!!

I'll review the tests after the core is good.

Uh, you raise NoLockDir, but use it to mean 'A config directory that is not locked' which is very odd. Please consider something that means what you need. Also see my ordering suggestion which may help you not to need this at all.

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote :

> Ok, actual review stuff:
> the docstring layout is wrong, please nuke the \.
>
> We should check for branches first, not config files, because branch locks are
> the common case and break-lock doesn't need to be slow.

break_lock() for wt, branch, repo gives no indication about whether it fails or succeeds.
Trying the conf files first was the easiest.

Regarding performance, I think we don't care at all, the user is supposed to first check that
the lock is not hold by a running process (or someone else) which requires seconds in the best case
or occur long after the lock has been created.

>
> This change is suspicous:
>
> 152 def _write_config_file(self):
> 153 - f = file(self._get_filename(), "wb")
> 154 + fname = self._get_filename()
> 155 + conf_dir = os.path.dirname(fname)
> 156 + ensure_config_dir_exists(conf_dir)
> 157 + f = file(fname, "wb")
> 158 try:
> 159 - osutils.copy_ownership_from_path(f.name)
> 160 + osutils.copy_ownership_from_path(fname)
> 161 self._get_parser().write(f)
> 162 finally:
> 163 f.close()
> 164
>
> It appears to be adding a new stat/mkdir check, at the wrong layer.

No adding there, code duplication removal instead, ensure_config_dir_exists() was called anyway.

>
> missing VWS:
>
> 172 + """
> 173 + lock_name = 'lock'

Fixed.
>
>
>
> Ditto here:
>
> 181 + def lock_write(self, token=None):
> 182 + if self.lock is None:
> 183 + ensure_config_dir_exists(self.dir)
> 184 + self._lock = lockdir.LockDir(self.transport,
> self.lock_name)
> 185 + self._lock.lock_write(token)
> 186 + return lock.LogicalLockResult(self.unlock)
>
> If the dir doesn't exist, something is wrong - we really shouldn't have gotten
> this far without it, should we?

When the config file didn't exist, the config dir needs to be created.

> Docstrings!!!

A bit terse but I will add some.

> Uh, you raise NoLockDir, but use it to mean 'A config directory that is not
> locked' which is very odd.
> Please consider something that means what you need.

I need something that means: "Oh, I wanted to break a lock there but there is no lock dir there,
surely I can't break a lock in that case". I fail to see the oddness :-/

Revision history for this message
Vincent Ladeuil (vila) wrote :

> As a comment, without having really read the code thoroughly.
>
> How does this handle stuff like 2 branches locking concurrently
> locations.conf. I don't know how often we do it internally, though.

TestLockableConfig.test_writes_are_serialized

>
> I think lots of filesystem locks on the bazaar directory could adversely
> affect performance on Windows. IME locking isn't too expensive if you do
> it 1 or 2 times. But if you lock and unlock on every attribute that gets
> set, then it probably starts to be an issue.

The actual code is not correct, it allows concurrent writers. If higher levels do too many updates of config variables it's a problem in the higher levels, we could imagine holding the updates until the last one, but
this can't addressed here.

Correctness comes before performance, there are many things we can do to address performance but it's way out of scope for this proposal IMHO.

>
> On a Windows host:
> $ TIMEIT -s "b = Branch.open('.')" "b.lock_write(); b.unlock()"
> 10.5msec
>
> On an Ubuntu VM on the same machine:
> $ TIMEIT -s "b = Branch.open('.')" "b.lock_write(); b.unlock()"
> 1.55msec

Thanks, good data point, but still, I haven't seen code doing massive updates of config variables either...

5327. By Vincent Ladeuil

Revert the lock scope to a sane value.

* bzrlib/tests/test_config.py:
(TestLockableConfig.test_writes_are_serialized)
(TestLockableConfig.test_read_while_writing): Fix the fallouts.

* bzrlib/config.py:
(LockableConfig): Wrong idea, the lock needs to be taken arond the
whole value update call, reducing the lock scope to
_write_config_file exclude the config file re-read.
(GlobalConfig.set_user_option, GlobalConfig.set_alias)
(GlobalConfig.unset_alias, LocationConfig.set_user_option): These
methods needs to be decorated with needs_write_lock to enforce the
design constraints (lock, re-read config, set new value, write
config, unlock).

5328. By Vincent Ladeuil

Fix wrong bug number and clarify NEWS entries.

5329. By Vincent Ladeuil

Fix docstring.

5330. By Vincent Ladeuil

Further clarify NEWS entry.

5331. By Vincent Ladeuil

Add a test to help LockableConfig daughter classes identify methods that needs to be decorated.

5332. By Vincent Ladeuil

Implement the --conf option for break-lock as per lifeless suggestion.

* bzrlib/errors.py:
(NoLockDir): Useless, deleted.

* bzrlib/config.py:
(LockableConfig.unlock): NoLockDir is useless, break_lock()
silenty succeeds if the directory doesn't exist.

* bzrlib/tests/blackbox/test_break_lock.py:
Tweak the tests.

* bzrlib/builtins.py:
(cmd_break_lock): Fix docstring, add a --conf option to deal with
config files.

5333. By Vincent Ladeuil

Final cleanup.

5334. By Vincent Ladeuil

Clarify lock scope.

5335. By Vincent Ladeuil

Use --config instead of --conf for break-lock.

Unmerged revisions

5335. By Vincent Ladeuil

Use --config instead of --conf for break-lock.

5334. By Vincent Ladeuil

Clarify lock scope.

5333. By Vincent Ladeuil

Final cleanup.

5332. By Vincent Ladeuil

Implement the --conf option for break-lock as per lifeless suggestion.

* bzrlib/errors.py:
(NoLockDir): Useless, deleted.

* bzrlib/config.py:
(LockableConfig.unlock): NoLockDir is useless, break_lock()
silenty succeeds if the directory doesn't exist.

* bzrlib/tests/blackbox/test_break_lock.py:
Tweak the tests.

* bzrlib/builtins.py:
(cmd_break_lock): Fix docstring, add a --conf option to deal with
config files.

5331. By Vincent Ladeuil

Add a test to help LockableConfig daughter classes identify methods that needs to be decorated.

5330. By Vincent Ladeuil

Further clarify NEWS entry.

5329. By Vincent Ladeuil

Fix docstring.

5328. By Vincent Ladeuil

Fix wrong bug number and clarify NEWS entries.

5327. By Vincent Ladeuil

Revert the lock scope to a sane value.

* bzrlib/tests/test_config.py:
(TestLockableConfig.test_writes_are_serialized)
(TestLockableConfig.test_read_while_writing): Fix the fallouts.

* bzrlib/config.py:
(LockableConfig): Wrong idea, the lock needs to be taken arond the
whole value update call, reducing the lock scope to
_write_config_file exclude the config file re-read.
(GlobalConfig.set_user_option, GlobalConfig.set_alias)
(GlobalConfig.unset_alias, LocationConfig.set_user_option): These
methods needs to be decorated with needs_write_lock to enforce the
design constraints (lock, re-read config, set new value, write
config, unlock).

5326. By Vincent Ladeuil

Add some comments.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2010-06-28 21:45:10 +0000
+++ NEWS 2010-06-30 14:21:34 +0000
@@ -36,6 +36,15 @@
36New Features36New Features
37************37************
3838
39* ``bzr break-lock`` now defines a --config parameter that is required
40 to break locks on config files. (Vincent Ladeuil, #525571)
41
42* ``bzrlib.config.LockableConfig`` is a base class for config files that
43 needs to be protected against multiple writers. All methods that
44 change a configuration variable value must be decorated with
45 @needs_write_lock (set_option() for example).
46 (Vincent Ladeuil, #525571)
47
39* Support ``--directory`` option for a number of additional commands:48* Support ``--directory`` option for a number of additional commands:
40 conflicts, merge-directive, missing, resolve, shelve, switch,49 conflicts, merge-directive, missing, resolve, shelve, switch,
41 unshelve, whoami. (Martin von Gagern, #527878)50 unshelve, whoami. (Martin von Gagern, #527878)
@@ -62,12 +71,18 @@
62 or pull location in locations.conf or branch.conf.71 or pull location in locations.conf or branch.conf.
63 (Gordon Tyler, #534787)72 (Gordon Tyler, #534787)
6473
74<<<<<<< TREE
65* ``BzrDir.find_branches`` should ignore branches with missing repositories.75* ``BzrDir.find_branches`` should ignore branches with missing repositories.
66 (Marius Kruger, Robert Collins)76 (Marius Kruger, Robert Collins)
6777
68* ``BzrDir.find_bzrdirs`` should ignore dirs that raises PermissionDenied.78* ``BzrDir.find_bzrdirs`` should ignore dirs that raises PermissionDenied.
69 (Marius Kruger, Robert Collins)79 (Marius Kruger, Robert Collins)
7080
81=======
82* Configuration files in ``${BZR_HOME}`` are now protected
83 against concurrent writers. (Vincent Ladeuil, #525571)
84
85>>>>>>> MERGE-SOURCE
71* Ensure that wrong path specifications in ``BZR_PLUGINS_AT`` display86* Ensure that wrong path specifications in ``BZR_PLUGINS_AT`` display
72 proper error messages. (Vincent Ladeuil, #591215)87 proper error messages. (Vincent Ladeuil, #591215)
7388
7489
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2010-06-23 08:19:28 +0000
+++ bzrlib/builtins.py 2010-06-30 14:21:34 +0000
@@ -4830,7 +4830,10 @@
48304830
48314831
4832class cmd_break_lock(Command):4832class cmd_break_lock(Command):
4833 __doc__ = """Break a dead lock on a repository, branch or working directory.4833 __doc__ = """Break a dead lock.
4834
4835 This command breaks a lock on a repository, branch, working directory or
4836 config file.
48344837
4835 CAUTION: Locks should only be broken when you are sure that the process4838 CAUTION: Locks should only be broken when you are sure that the process
4836 holding the lock has been stopped.4839 holding the lock has been stopped.
@@ -4841,17 +4844,27 @@
4841 :Examples:4844 :Examples:
4842 bzr break-lock4845 bzr break-lock
4843 bzr break-lock bzr+ssh://example.com/bzr/foo4846 bzr break-lock bzr+ssh://example.com/bzr/foo
4847 bzr break-lock --conf ~/.bazaar
4844 """4848 """
4849
4845 takes_args = ['location?']4850 takes_args = ['location?']
4851 takes_options = [
4852 Option('conf',
4853 help='LOCATION is a directory containing configuration files.'),
4854 ]
48464855
4847 def run(self, location=None, show=False):4856 def run(self, location=None, conf=False):
4848 if location is None:4857 if location is None:
4849 location = u'.'4858 location = u'.'
4850 control, relpath = bzrdir.BzrDir.open_containing(location)4859 if conf:
4851 try:4860 c = config.LockableConfig(lambda : location)
4852 control.break_lock()4861 c.break_lock()
4853 except NotImplementedError:4862 else:
4854 pass4863 control, relpath = bzrdir.BzrDir.open_containing(location)
4864 try:
4865 control.break_lock()
4866 except NotImplementedError:
4867 pass
48554868
48564869
4857class cmd_wait_until_signalled(Command):4870class cmd_wait_until_signalled(Command):
48584871
=== modified file 'bzrlib/config.py'
--- bzrlib/config.py 2010-06-02 04:50:35 +0000
+++ bzrlib/config.py 2010-06-30 14:21:34 +0000
@@ -62,9 +62,11 @@
62up=pull62up=pull
63"""63"""
6464
65from cStringIO import StringIO
65import os66import os
66import sys67import sys
6768
69from bzrlib.decorators import needs_write_lock
68from bzrlib.lazy_import import lazy_import70from bzrlib.lazy_import import lazy_import
69lazy_import(globals(), """71lazy_import(globals(), """
70import errno72import errno
@@ -76,11 +78,14 @@
76from bzrlib import (78from bzrlib import (
77 debug,79 debug,
78 errors,80 errors,
81 lock,
82 lockdir,
79 mail_client,83 mail_client,
80 osutils,84 osutils,
81 registry,85 registry,
82 symbol_versioning,86 symbol_versioning,
83 trace,87 trace,
88 transport,
84 ui,89 ui,
85 urlutils,90 urlutils,
86 win32utils,91 win32utils,
@@ -356,18 +361,30 @@
356 self._get_filename = get_filename361 self._get_filename = get_filename
357 self._parser = None362 self._parser = None
358363
359 def _get_parser(self, file=None):364 def _get_parser(self, infile=None):
360 if self._parser is not None:365 if self._parser is not None:
361 return self._parser366 return self._parser
362 if file is None:367 # Do we have a file name for the config file
363 input = self._get_filename()368 if self._get_filename is None:
364 else:369 fname = None
365 input = file370 else:
371 fname = self._get_filename()
372 # Do we have a content for the config file ?
373 if infile is None:
374 fname_or_content = fname
375 else:
376 fname_or_content = infile
377 # Build the ConfigObj
378 p = None
366 try:379 try:
367 self._parser = ConfigObj(input, encoding='utf-8')380 p = ConfigObj(fname_or_content, encoding='utf-8')
368 except configobj.ConfigObjError, e:381 except configobj.ConfigObjError, e:
369 raise errors.ParseConfigError(e.errors, e.config.filename)382 raise errors.ParseConfigError(e.errors, e.config.filename)
370 return self._parser383 if p is not None and fname is not None:
384 # Make sure p.reload() will use the right file name
385 p.filename = fname
386 self._parser = p
387 return p
371388
372 def _get_matching_sections(self):389 def _get_matching_sections(self):
373 """Return an ordered list of (section_name, extra_path) pairs.390 """Return an ordered list of (section_name, extra_path) pairs.
@@ -478,27 +495,76 @@
478 return self.get_user_option('nickname')495 return self.get_user_option('nickname')
479496
480 def _write_config_file(self):497 def _write_config_file(self):
481 f = file(self._get_filename(), "wb")498 fname = self._get_filename()
499 conf_dir = os.path.dirname(fname)
500 ensure_config_dir_exists(conf_dir)
501 f = file(fname, "wb")
482 try:502 try:
483 osutils.copy_ownership_from_path(f.name)503 osutils.copy_ownership_from_path(fname)
484 self._get_parser().write(f)504 self._get_parser().write(f)
485 finally:505 finally:
486 f.close()506 f.close()
487507
488508
489class GlobalConfig(IniBasedConfig):509class LockableConfig(IniBasedConfig):
510 """A configuration needing explicit locking for access.
511
512 If several processes try to write the config file, the accesses need to be
513 serialized.
514 """
515
516 lock_name = 'lock'
517
518 def __init__(self, get_filename):
519 super(LockableConfig, self).__init__(get_filename)
520 self.dir = osutils.dirname(osutils.safe_unicode(self._get_filename()))
521 self.transport = transport.get_transport(self.dir)
522 self._lock = None
523
524 def lock_write(self, token=None):
525 if self._lock is None:
526 ensure_config_dir_exists(self.dir)
527 self._lock = lockdir.LockDir(self.transport, self.lock_name)
528 self._lock.lock_write(token)
529 return lock.LogicalLockResult(self.unlock)
530
531 def unlock(self):
532 self._lock.unlock()
533
534 def break_lock(self):
535 if self._lock is None:
536 self._lock = lockdir.LockDir(self.transport, self.lock_name)
537 self._lock.break_lock()
538
539 def _write_config_file(self):
540 if self._lock is None or not self._lock.is_held:
541 # NB: if the following exception is raised it probably means a
542 # missing @needs_write_lock decorator on one of the callers.
543 raise errors.ObjectNotLocked(self)
544 fname = self._get_filename()
545 f = StringIO()
546 p = self._get_parser()
547 p.write(f)
548 self.transport.put_bytes(os.path.basename(fname), f.getvalue())
549 # Make sure p.reload() will use the right file name
550 p.filename = fname
551 osutils.copy_ownership_from_path(fname)
552
553
554class GlobalConfig(LockableConfig):
490 """The configuration that should be used for a specific location."""555 """The configuration that should be used for a specific location."""
491556
492 def get_editor(self):
493 return self._get_user_option('editor')
494
495 def __init__(self):557 def __init__(self):
496 super(GlobalConfig, self).__init__(config_filename)558 super(GlobalConfig, self).__init__(config_filename)
497559
560 @needs_write_lock
498 def set_user_option(self, option, value):561 def set_user_option(self, option, value):
499 """Save option and its value in the configuration."""562 """Save option and its value in the configuration."""
500 self._set_option(option, value, 'DEFAULT')563 self._set_option(option, value, 'DEFAULT')
501564
565 def get_editor(self):
566 return self._get_user_option('editor')
567
502 def get_aliases(self):568 def get_aliases(self):
503 """Return the aliases section."""569 """Return the aliases section."""
504 if 'ALIASES' in self._get_parser():570 if 'ALIASES' in self._get_parser():
@@ -506,10 +572,12 @@
506 else:572 else:
507 return {}573 return {}
508574
575 @needs_write_lock
509 def set_alias(self, alias_name, alias_command):576 def set_alias(self, alias_name, alias_command):
510 """Save the alias in the configuration."""577 """Save the alias in the configuration."""
511 self._set_option(alias_name, alias_command, 'ALIASES')578 self._set_option(alias_name, alias_command, 'ALIASES')
512579
580 @needs_write_lock
513 def unset_alias(self, alias_name):581 def unset_alias(self, alias_name):
514 """Unset an existing alias."""582 """Unset an existing alias."""
515 aliases = self._get_parser().get('ALIASES')583 aliases = self._get_parser().get('ALIASES')
@@ -519,15 +587,13 @@
519 self._write_config_file()587 self._write_config_file()
520588
521 def _set_option(self, option, value, section):589 def _set_option(self, option, value, section):
522 # FIXME: RBC 20051029 This should refresh the parser and also take a590 if self._parser is not None:
523 # file lock on bazaar.conf.591 self._parser.reload()
524 conf_dir = os.path.dirname(self._get_filename())
525 ensure_config_dir_exists(conf_dir)
526 self._get_parser().setdefault(section, {})[option] = value592 self._get_parser().setdefault(section, {})[option] = value
527 self._write_config_file()593 self._write_config_file()
528594
529595
530class LocationConfig(IniBasedConfig):596class LocationConfig(LockableConfig):
531 """A configuration object that gives the policy for a location."""597 """A configuration object that gives the policy for a location."""
532598
533 def __init__(self, location):599 def __init__(self, location):
@@ -643,6 +709,7 @@
643 if policy_key in self._get_parser()[section]:709 if policy_key in self._get_parser()[section]:
644 del self._get_parser()[section][policy_key]710 del self._get_parser()[section][policy_key]
645711
712 @needs_write_lock
646 def set_user_option(self, option, value, store=STORE_LOCATION):713 def set_user_option(self, option, value, store=STORE_LOCATION):
647 """Save option and its value in the configuration."""714 """Save option and its value in the configuration."""
648 if store not in [STORE_LOCATION,715 if store not in [STORE_LOCATION,
@@ -650,10 +717,8 @@
650 STORE_LOCATION_APPENDPATH]:717 STORE_LOCATION_APPENDPATH]:
651 raise ValueError('bad storage policy %r for %r' %718 raise ValueError('bad storage policy %r for %r' %
652 (store, option))719 (store, option))
653 # FIXME: RBC 20051029 This should refresh the parser and also take a720 if self._parser is not None:
654 # file lock on locations.conf.721 self._parser.reload()
655 conf_dir = os.path.dirname(self._get_filename())
656 ensure_config_dir_exists(conf_dir)
657 location = self.location722 location = self.location
658 if location.endswith('/'):723 if location.endswith('/'):
659 location = location[:-1]724 location = location[:-1]
660725
=== modified file 'bzrlib/tests/blackbox/test_break_lock.py'
--- bzrlib/tests/blackbox/test_break_lock.py 2010-06-11 07:32:12 +0000
+++ bzrlib/tests/blackbox/test_break_lock.py 2010-06-30 14:21:34 +0000
@@ -18,17 +18,18 @@
1818
19import os19import os
2020
21import bzrlib
22from bzrlib import (21from bzrlib import (
22 branch,
23 bzrdir,
24 config,
23 errors,25 errors,
24 lockdir,26 lockdir,
27 osutils,
28 tests,
25 )29 )
26from bzrlib.branch import Branch30
27from bzrlib.bzrdir import BzrDir31
28from bzrlib.tests import TestCaseWithTransport32class TestBreakLock(tests.TestCaseWithTransport):
29
30
31class TestBreakLock(TestCaseWithTransport):
3233
33 # General principal for break-lock: All the elements that might be locked34 # General principal for break-lock: All the elements that might be locked
34 # by a bzr operation on PATH, are candidates that break-lock may unlock.35 # by a bzr operation on PATH, are candidates that break-lock may unlock.
@@ -52,14 +53,14 @@
52 'repo/',53 'repo/',
53 'repo/branch/',54 'repo/branch/',
54 'checkout/'])55 'checkout/'])
55 bzrlib.bzrdir.BzrDir.create('master-repo').create_repository()56 bzrdir.BzrDir.create('master-repo').create_repository()
56 self.master_branch = bzrlib.bzrdir.BzrDir.create_branch_convenience(57 self.master_branch = bzrdir.BzrDir.create_branch_convenience(
57 'master-repo/master-branch')58 'master-repo/master-branch')
58 bzrlib.bzrdir.BzrDir.create('repo').create_repository()59 bzrdir.BzrDir.create('repo').create_repository()
59 local_branch = bzrlib.bzrdir.BzrDir.create_branch_convenience('repo/branch')60 local_branch = bzrdir.BzrDir.create_branch_convenience('repo/branch')
60 local_branch.bind(self.master_branch)61 local_branch.bind(self.master_branch)
61 checkoutdir = bzrlib.bzrdir.BzrDir.create('checkout')62 checkoutdir = bzrdir.BzrDir.create('checkout')
62 bzrlib.branch.BranchReferenceFormat().initialize(63 branch.BranchReferenceFormat().initialize(
63 checkoutdir, target_branch=local_branch)64 checkoutdir, target_branch=local_branch)
64 self.wt = checkoutdir.create_workingtree()65 self.wt = checkoutdir.create_workingtree()
6566
@@ -73,7 +74,7 @@
73 # however, we dont test breaking the working tree because we74 # however, we dont test breaking the working tree because we
74 # cannot accurately do so right now: the dirstate lock is held75 # cannot accurately do so right now: the dirstate lock is held
75 # by an os lock, and we need to spawn a separate process to lock it76 # by an os lock, and we need to spawn a separate process to lock it
76 # thne kill -9 it.77 # then kill -9 it.
77 # sketch of test:78 # sketch of test:
78 # lock most of the dir:79 # lock most of the dir:
79 self.wt.branch.lock_write()80 self.wt.branch.lock_write()
@@ -82,22 +83,45 @@
82 # we need 5 yes's - wt, branch, repo, bound branch, bound repo.83 # we need 5 yes's - wt, branch, repo, bound branch, bound repo.
83 self.run_bzr('break-lock checkout', stdin="y\ny\ny\ny\n")84 self.run_bzr('break-lock checkout', stdin="y\ny\ny\ny\n")
84 # a new tree instance should be lockable85 # a new tree instance should be lockable
85 branch = bzrlib.branch.Branch.open('checkout')86 b = branch.Branch.open('checkout')
86 branch.lock_write()87 b.lock_write()
87 branch.unlock()88 b.unlock()
88 # and a new instance of the master branch89 # and a new instance of the master branch
89 mb = branch.get_master_branch()90 mb = b.get_master_branch()
90 mb.lock_write()91 mb.lock_write()
91 mb.unlock()92 mb.unlock()
92 self.assertRaises(errors.LockBroken, self.wt.unlock)93 self.assertRaises(errors.LockBroken, self.wt.unlock)
93 self.assertRaises(errors.LockBroken, self.master_branch.unlock)94 self.assertRaises(errors.LockBroken, self.master_branch.unlock)
9495
9596
96class TestBreakLockOldBranch(TestCaseWithTransport):97class TestBreakLockOldBranch(tests.TestCaseWithTransport):
9798
98 def test_break_lock_format_5_bzrdir(self):99 def test_break_lock_format_5_bzrdir(self):
99 # break lock on a format 5 bzrdir should just return100 # break lock on a format 5 bzrdir should just return
100 self.make_branch_and_tree('foo', format=bzrlib.bzrdir.BzrDirFormat5())101 self.make_branch_and_tree('foo', format=bzrdir.BzrDirFormat5())
101 out, err = self.run_bzr('break-lock foo')102 out, err = self.run_bzr('break-lock foo')
102 self.assertEqual('', out)103 self.assertEqual('', out)
103 self.assertEqual('', err)104 self.assertEqual('', err)
105
106
107class TestConfigBreakLock(tests.TestCaseWithTransport):
108
109 def create_pending_lock(self):
110 def config_file_name():
111 return './my.conf'
112 self.build_tree_contents([(config_file_name(), '[DEFAULT]\none=1\n')])
113 conf = config.LockableConfig(config_file_name)
114 conf.lock_write()
115 return conf
116
117 def test_create_pending_lock(self):
118 conf = self.create_pending_lock()
119 self.addCleanup(conf.unlock)
120 self.assertTrue(conf._lock.is_held)
121
122 def test_break_lock(self):
123 conf = self.create_pending_lock()
124 self.run_bzr('break-lock --conf %s'
125 % osutils.dirname(conf._get_filename()),
126 stdin="y\n")
127 self.assertRaises(errors.LockBroken, conf.unlock)
104128
=== modified file 'bzrlib/tests/test_commands.py'
--- bzrlib/tests/test_commands.py 2010-05-27 21:16:48 +0000
+++ bzrlib/tests/test_commands.py 2010-06-30 14:21:34 +0000
@@ -97,7 +97,7 @@
97 def _get_config(self, config_text):97 def _get_config(self, config_text):
98 my_config = config.GlobalConfig()98 my_config = config.GlobalConfig()
99 config_file = StringIO(config_text.encode('utf-8'))99 config_file = StringIO(config_text.encode('utf-8'))
100 my_config._parser = my_config._get_parser(file=config_file)100 my_config._parser = my_config._get_parser(infile=config_file)
101 return my_config101 return my_config
102102
103 def test_simple(self):103 def test_simple(self):
104104
=== modified file 'bzrlib/tests/test_config.py'
--- bzrlib/tests/test_config.py 2010-04-23 07:15:23 +0000
+++ bzrlib/tests/test_config.py 2010-06-30 14:21:34 +0000
@@ -19,6 +19,7 @@
19from cStringIO import StringIO19from cStringIO import StringIO
20import os20import os
21import sys21import sys
22import threading
2223
23#import bzrlib specific imports here24#import bzrlib specific imports here
24from bzrlib import (25from bzrlib import (
@@ -38,6 +39,32 @@
38from bzrlib.util.configobj import configobj39from bzrlib.util.configobj import configobj
3940
4041
42def lockable_config_scenarios():
43 return [
44 ('global',
45 {'config_file_name': config.config_filename,
46 'config_class': config.GlobalConfig,
47 'config_args': [],
48 'config_section': 'DEFAULT'}),
49 ('locations',
50 {'config_file_name': config.locations_config_filename,
51 'config_class': config.LocationConfig,
52 'config_args': ['.'],
53 'config_section': '.'}),]
54
55
56def load_tests(standard_tests, module, loader):
57 suite = loader.suiteClass()
58
59 lc_tests, remaining_tests = tests.split_suite_by_condition(
60 standard_tests, tests.condition_isinstance((
61 TestLockableConfig,
62 )))
63 tests.multiply_tests(lc_tests, lockable_config_scenarios(), suite)
64 suite.addTest(remaining_tests)
65 return suite
66
67
41sample_long_alias="log -r-15..-1 --line"68sample_long_alias="log -r-15..-1 --line"
42sample_config_text = u"""69sample_config_text = u"""
43[DEFAULT]70[DEFAULT]
@@ -129,6 +156,9 @@
129 self._calls.append(('keys',))156 self._calls.append(('keys',))
130 return []157 return []
131158
159 def reload(self):
160 self._calls.append(('reload',))
161
132 def write(self, arg):162 def write(self, arg):
133 self._calls.append(('write',))163 self._calls.append(('write',))
134164
@@ -371,7 +401,7 @@
371401
372 def make_config_parser(self, s):402 def make_config_parser(self, s):
373 conf = config.IniBasedConfig(None)403 conf = config.IniBasedConfig(None)
374 parser = conf._get_parser(file=StringIO(s.encode('utf-8')))404 parser = conf._get_parser(infile=StringIO(s.encode('utf-8')))
375 return conf, parser405 return conf, parser
376406
377407
@@ -384,16 +414,144 @@
384 config_file = StringIO(sample_config_text.encode('utf-8'))414 config_file = StringIO(sample_config_text.encode('utf-8'))
385 my_config = config.IniBasedConfig(None)415 my_config = config.IniBasedConfig(None)
386 self.failUnless(416 self.failUnless(
387 isinstance(my_config._get_parser(file=config_file),417 isinstance(my_config._get_parser(infile=config_file),
388 configobj.ConfigObj))418 configobj.ConfigObj))
389419
390 def test_cached(self):420 def test_cached(self):
391 config_file = StringIO(sample_config_text.encode('utf-8'))421 config_file = StringIO(sample_config_text.encode('utf-8'))
392 my_config = config.IniBasedConfig(None)422 my_config = config.IniBasedConfig(None)
393 parser = my_config._get_parser(file=config_file)423 parser = my_config._get_parser(infile=config_file)
394 self.failUnless(my_config._get_parser() is parser)424 self.failUnless(my_config._get_parser() is parser)
395425
396426
427class TestLockableConfig(tests.TestCaseInTempDir):
428
429 # Set by load_tests
430 config_file_name = None
431 config_class = None
432 config_args = None
433 config_section = None
434
435 def setUp(self):
436 super(TestLockableConfig, self).setUp()
437 config.ensure_config_dir_exists()
438 text = '[%s]\none=1\ntwo=2\n' % (self.config_section,)
439 self.build_tree_contents([(self.config_file_name(), text)])
440
441 def create_config(self):
442 return self.config_class(*self.config_args)
443
444 def test_simple_read_access(self):
445 c = self.create_config()
446 self.assertEquals('1', c.get_user_option('one'))
447
448 def test_simple_write_access(self):
449 c = self.create_config()
450 c.set_user_option('one', 'one')
451 self.assertEquals('one', c.get_user_option('one'))
452
453 def test_unlocked_config(self):
454 c = self.create_config()
455 self.assertRaises(errors.ObjectNotLocked, c._write_config_file)
456
457 def test_listen_to_the_last_speaker(self):
458 c1 = self.create_config()
459 c2 = self.create_config()
460 c1.set_user_option('one', 'ONE')
461 c2.set_user_option('two', 'TWO')
462 self.assertEquals('ONE', c1.get_user_option('one'))
463 self.assertEquals('TWO', c2.get_user_option('two'))
464 # The second update respect the first one
465 self.assertEquals('ONE', c2.get_user_option('one'))
466
467 def test_last_speaker_wins(self):
468 # If the same config is not shared, the same variable modified twice
469 # can only see a single result.
470 c1 = self.create_config()
471 c2 = self.create_config()
472 c1.set_user_option('one', 'c1')
473 c2.set_user_option('one', 'c2')
474 self.assertEquals('c2', c2.get_user_option('one'))
475 # The first modification is still available until another refresh
476 # occur
477 self.assertEquals('c1', c1.get_user_option('one'))
478 c1.set_user_option('two', 'done')
479 self.assertEquals('c2', c1.get_user_option('one'))
480
481 def test_writes_are_serialized(self):
482 c1 = self.create_config()
483 c2 = self.create_config()
484
485 # We spawn a thread that will pause *during* the write
486 before_writing = threading.Event()
487 after_writing = threading.Event()
488 writing_done = threading.Event()
489 c1_orig = c1._write_config_file
490 def c1_write_config_file():
491 before_writing.set()
492 c1_orig()
493 # The lock is held we wait for the main thread to decide when to
494 # continue
495 after_writing.wait()
496 c1._write_config_file = c1_write_config_file
497 def c1_set_option():
498 c1.set_user_option('one', 'c1')
499 writing_done.set()
500 t1 = threading.Thread(target=c1_set_option)
501 # Collect the thread after the test
502 self.addCleanup(t1.join)
503 # Be ready to unblock the thread if the test goes wrong
504 self.addCleanup(after_writing.set)
505 t1.start()
506 before_writing.wait()
507 self.assertTrue(c1._lock.is_held)
508 self.assertRaises(errors.LockContention,
509 c2.set_user_option, 'one', 'c2')
510 self.assertEquals('c1', c1.get_user_option('one'))
511 # Let the lock be released
512 after_writing.set()
513 writing_done.wait()
514 c2.set_user_option('one', 'c2')
515 self.assertEquals('c2', c2.get_user_option('one'))
516
517 def test_read_while_writing(self):
518 c1 = self.create_config()
519 # We spawn a thread that will pause *during* the write
520 ready_to_write = threading.Event()
521 do_writing = threading.Event()
522 writing_done = threading.Event()
523 c1_orig = c1._write_config_file
524 def c1_write_config_file():
525 ready_to_write.set()
526 # The lock is held we wait for the main thread to decide when to
527 # continue
528 do_writing.wait()
529 c1_orig()
530 writing_done.set()
531 c1._write_config_file = c1_write_config_file
532 def c1_set_option():
533 c1.set_user_option('one', 'c1')
534 t1 = threading.Thread(target=c1_set_option)
535 # Collect the thread after the test
536 self.addCleanup(t1.join)
537 # Be ready to unblock the thread if the test goes wrong
538 self.addCleanup(do_writing.set)
539 t1.start()
540 # Ensure the thread is ready to write
541 ready_to_write.wait()
542 self.assertTrue(c1._lock.is_held)
543 self.assertEquals('c1', c1.get_user_option('one'))
544 # If we read during the write, we get the old value
545 c2 = self.create_config()
546 self.assertEquals('1', c2.get_user_option('one'))
547 # Let the writing occur and ensure it occurred
548 do_writing.set()
549 writing_done.wait()
550 # Now we get the updated value
551 c3 = self.create_config()
552 self.assertEquals('c1', c3.get_user_option('one'))
553
554
397class TestGetUserOptionAs(TestIniConfig):555class TestGetUserOptionAs(TestIniConfig):
398556
399 def test_get_user_option_as_bool(self):557 def test_get_user_option_as_bool(self):
@@ -583,26 +741,26 @@
583 def test_user_id(self):741 def test_user_id(self):
584 config_file = StringIO(sample_config_text.encode('utf-8'))742 config_file = StringIO(sample_config_text.encode('utf-8'))
585 my_config = config.GlobalConfig()743 my_config = config.GlobalConfig()
586 my_config._parser = my_config._get_parser(file=config_file)744 my_config._parser = my_config._get_parser(infile=config_file)
587 self.assertEqual(u"Erik B\u00e5gfors <erik@bagfors.nu>",745 self.assertEqual(u"Erik B\u00e5gfors <erik@bagfors.nu>",
588 my_config._get_user_id())746 my_config._get_user_id())
589747
590 def test_absent_user_id(self):748 def test_absent_user_id(self):
591 config_file = StringIO("")749 config_file = StringIO("")
592 my_config = config.GlobalConfig()750 my_config = config.GlobalConfig()
593 my_config._parser = my_config._get_parser(file=config_file)751 my_config._parser = my_config._get_parser(infile=config_file)
594 self.assertEqual(None, my_config._get_user_id())752 self.assertEqual(None, my_config._get_user_id())
595753
596 def test_configured_editor(self):754 def test_configured_editor(self):
597 config_file = StringIO(sample_config_text.encode('utf-8'))755 config_file = StringIO(sample_config_text.encode('utf-8'))
598 my_config = config.GlobalConfig()756 my_config = config.GlobalConfig()
599 my_config._parser = my_config._get_parser(file=config_file)757 my_config._parser = my_config._get_parser(infile=config_file)
600 self.assertEqual("vim", my_config.get_editor())758 self.assertEqual("vim", my_config.get_editor())
601759
602 def test_signatures_always(self):760 def test_signatures_always(self):
603 config_file = StringIO(sample_always_signatures)761 config_file = StringIO(sample_always_signatures)
604 my_config = config.GlobalConfig()762 my_config = config.GlobalConfig()
605 my_config._parser = my_config._get_parser(file=config_file)763 my_config._parser = my_config._get_parser(infile=config_file)
606 self.assertEqual(config.CHECK_NEVER,764 self.assertEqual(config.CHECK_NEVER,
607 my_config.signature_checking())765 my_config.signature_checking())
608 self.assertEqual(config.SIGN_ALWAYS,766 self.assertEqual(config.SIGN_ALWAYS,
@@ -612,7 +770,7 @@
612 def test_signatures_if_possible(self):770 def test_signatures_if_possible(self):
613 config_file = StringIO(sample_maybe_signatures)771 config_file = StringIO(sample_maybe_signatures)
614 my_config = config.GlobalConfig()772 my_config = config.GlobalConfig()
615 my_config._parser = my_config._get_parser(file=config_file)773 my_config._parser = my_config._get_parser(infile=config_file)
616 self.assertEqual(config.CHECK_NEVER,774 self.assertEqual(config.CHECK_NEVER,
617 my_config.signature_checking())775 my_config.signature_checking())
618 self.assertEqual(config.SIGN_WHEN_REQUIRED,776 self.assertEqual(config.SIGN_WHEN_REQUIRED,
@@ -622,7 +780,7 @@
622 def test_signatures_ignore(self):780 def test_signatures_ignore(self):
623 config_file = StringIO(sample_ignore_signatures)781 config_file = StringIO(sample_ignore_signatures)
624 my_config = config.GlobalConfig()782 my_config = config.GlobalConfig()
625 my_config._parser = my_config._get_parser(file=config_file)783 my_config._parser = my_config._get_parser(infile=config_file)
626 self.assertEqual(config.CHECK_ALWAYS,784 self.assertEqual(config.CHECK_ALWAYS,
627 my_config.signature_checking())785 my_config.signature_checking())
628 self.assertEqual(config.SIGN_NEVER,786 self.assertEqual(config.SIGN_NEVER,
@@ -632,7 +790,7 @@
632 def _get_sample_config(self):790 def _get_sample_config(self):
633 config_file = StringIO(sample_config_text.encode('utf-8'))791 config_file = StringIO(sample_config_text.encode('utf-8'))
634 my_config = config.GlobalConfig()792 my_config = config.GlobalConfig()
635 my_config._parser = my_config._get_parser(file=config_file)793 my_config._parser = my_config._get_parser(infile=config_file)
636 return my_config794 return my_config
637795
638 def test_gpg_signing_command(self):796 def test_gpg_signing_command(self):
@@ -643,7 +801,7 @@
643 def _get_empty_config(self):801 def _get_empty_config(self):
644 config_file = StringIO("")802 config_file = StringIO("")
645 my_config = config.GlobalConfig()803 my_config = config.GlobalConfig()
646 my_config._parser = my_config._get_parser(file=config_file)804 my_config._parser = my_config._get_parser(infile=config_file)
647 return my_config805 return my_config
648806
649 def test_gpg_signing_command_unset(self):807 def test_gpg_signing_command_unset(self):
@@ -745,10 +903,11 @@
745 [('__init__', config.locations_config_filename(),903 [('__init__', config.locations_config_filename(),
746 'utf-8')])904 'utf-8')])
747 config.ensure_config_dir_exists()905 config.ensure_config_dir_exists()
748 #os.mkdir(config.config_dir())
749 f = file(config.branches_config_filename(), 'wb')906 f = file(config.branches_config_filename(), 'wb')
750 f.write('')907 try:
751 f.close()908 f.write('')
909 finally:
910 f.close()
752 oldparserclass = config.ConfigObj911 oldparserclass = config.ConfigObj
753 config.ConfigObj = InstrumentedConfigObj912 config.ConfigObj = InstrumentedConfigObj
754 try:913 try:
@@ -995,10 +1154,15 @@
995 else:1154 else:
996 global_file = StringIO(global_config.encode('utf-8'))1155 global_file = StringIO(global_config.encode('utf-8'))
997 branches_file = StringIO(sample_branches_text.encode('utf-8'))1156 branches_file = StringIO(sample_branches_text.encode('utf-8'))
1157 # Make sure the locations config can be reloaded
1158 config.ensure_config_dir_exists()
1159 f = file(config.locations_config_filename(), 'wb')
1160 try:
1161 f.write(branches_file.getvalue())
1162 finally:
1163 f.close
998 self.my_config = config.BranchConfig(FakeBranch(location))1164 self.my_config = config.BranchConfig(FakeBranch(location))
999 # Force location config to use specified file
1000 self.my_location_config = self.my_config._get_location_config()1165 self.my_location_config = self.my_config._get_location_config()
1001 self.my_location_config._get_parser(branches_file)
1002 # Force global config to use specified file1166 # Force global config to use specified file
1003 self.my_config._get_global_config()._get_parser(global_file)1167 self.my_config._get_global_config()._get_parser(global_file)
10041168
@@ -1007,25 +1171,14 @@
1007 record = InstrumentedConfigObj("foo")1171 record = InstrumentedConfigObj("foo")
1008 self.my_location_config._parser = record1172 self.my_location_config._parser = record
10091173
1010 real_mkdir = os.mkdir1174 self.callDeprecated(['The recurse option is deprecated as of '
1011 self.created = False1175 '0.14. The section "/a/c" has been '
1012 def checked_mkdir(path, mode=0777):1176 'converted to use policies.'],
1013 self.log('making directory: %s', path)1177 self.my_config.set_user_option,
1014 real_mkdir(path, mode)1178 'foo', 'bar', store=config.STORE_LOCATION)
1015 self.created = True1179
10161180 self.assertEqual([('reload',),
1017 os.mkdir = checked_mkdir1181 ('__contains__', '/a/c'),
1018 try:
1019 self.callDeprecated(['The recurse option is deprecated as of '
1020 '0.14. The section "/a/c" has been '
1021 'converted to use policies.'],
1022 self.my_config.set_user_option,
1023 'foo', 'bar', store=config.STORE_LOCATION)
1024 finally:
1025 os.mkdir = real_mkdir
1026
1027 self.failUnless(self.created, 'Failed to create ~/.bazaar')
1028 self.assertEqual([('__contains__', '/a/c'),
1029 ('__contains__', '/a/c/'),1182 ('__contains__', '/a/c/'),
1030 ('__setitem__', '/a/c', {}),1183 ('__setitem__', '/a/c', {}),
1031 ('__getitem__', '/a/c'),1184 ('__getitem__', '/a/c'),
@@ -1083,10 +1236,13 @@
1083 if global_config is not None:1236 if global_config is not None:
1084 global_file = StringIO(global_config.encode('utf-8'))1237 global_file = StringIO(global_config.encode('utf-8'))
1085 my_config._get_global_config()._get_parser(global_file)1238 my_config._get_global_config()._get_parser(global_file)
1086 self.my_location_config = my_config._get_location_config()1239 lconf = my_config._get_location_config()
1087 if location_config is not None:1240 if location_config is not None:
1088 location_file = StringIO(location_config.encode('utf-8'))1241 location_file = StringIO(location_config.encode('utf-8'))
1089 self.my_location_config._get_parser(location_file)1242 lconf._get_parser(location_file)
1243 # Make sure the config can be reloaded
1244 lconf._parser.filename = config.locations_config_filename()
1245 self.my_location_config = lconf
1090 if branch_data_config is not None:1246 if branch_data_config is not None:
1091 my_config.branch.control_files.files['branch.conf'] = \1247 my_config.branch.control_files.files['branch.conf'] = \
1092 branch_data_config1248 branch_data_config