Merge lp:~vila/bzr/525571-lock-bazaar-conf-files into lp:bzr
- 525571-lock-bazaar-conf-files
- Merge into bzr.dev
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 | ||||
Related bugs: |
|
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.
Commit message
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.
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.
John A Meinel (jameinel) wrote : | # |
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
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
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_
153 - f = file(self.
154 + fname = self._get_
155 + conf_dir = os.path.
156 + ensure_
157 + f = file(fname, "wb")
158 try:
159 - osutils.
160 + osutils.
161 self._get_
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_
184 + self._lock = lockdir.
185 + self._lock.
186 + return lock.LogicalLoc
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.
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_
> 153 - f = file(self.
> 154 + fname = self._get_
> 155 + conf_dir = os.path.
> 156 + ensure_
> 157 + f = file(fname, "wb")
> 158 try:
> 159 - osutils.
> 160 + osutils.
> 161 self._get_
> 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_
>
> 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_
> 184 + self._lock = lockdir.
> self.lock_name)
> 185 + self._lock.
> 186 + return lock.LogicalLoc
>
> 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 :-/
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.
TestLockableCon
>
> 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
1 | === modified file 'NEWS' | |||
2 | --- NEWS 2010-06-28 21:45:10 +0000 | |||
3 | +++ NEWS 2010-06-30 14:21:34 +0000 | |||
4 | @@ -36,6 +36,15 @@ | |||
5 | 36 | New Features | 36 | New Features |
6 | 37 | ************ | 37 | ************ |
7 | 38 | 38 | ||
8 | 39 | * ``bzr break-lock`` now defines a --config parameter that is required | ||
9 | 40 | to break locks on config files. (Vincent Ladeuil, #525571) | ||
10 | 41 | |||
11 | 42 | * ``bzrlib.config.LockableConfig`` is a base class for config files that | ||
12 | 43 | needs to be protected against multiple writers. All methods that | ||
13 | 44 | change a configuration variable value must be decorated with | ||
14 | 45 | @needs_write_lock (set_option() for example). | ||
15 | 46 | (Vincent Ladeuil, #525571) | ||
16 | 47 | |||
17 | 39 | * Support ``--directory`` option for a number of additional commands: | 48 | * Support ``--directory`` option for a number of additional commands: |
18 | 40 | conflicts, merge-directive, missing, resolve, shelve, switch, | 49 | conflicts, merge-directive, missing, resolve, shelve, switch, |
19 | 41 | unshelve, whoami. (Martin von Gagern, #527878) | 50 | unshelve, whoami. (Martin von Gagern, #527878) |
20 | @@ -62,12 +71,18 @@ | |||
21 | 62 | or pull location in locations.conf or branch.conf. | 71 | or pull location in locations.conf or branch.conf. |
22 | 63 | (Gordon Tyler, #534787) | 72 | (Gordon Tyler, #534787) |
23 | 64 | 73 | ||
24 | 74 | <<<<<<< TREE | ||
25 | 65 | * ``BzrDir.find_branches`` should ignore branches with missing repositories. | 75 | * ``BzrDir.find_branches`` should ignore branches with missing repositories. |
26 | 66 | (Marius Kruger, Robert Collins) | 76 | (Marius Kruger, Robert Collins) |
27 | 67 | 77 | ||
28 | 68 | * ``BzrDir.find_bzrdirs`` should ignore dirs that raises PermissionDenied. | 78 | * ``BzrDir.find_bzrdirs`` should ignore dirs that raises PermissionDenied. |
29 | 69 | (Marius Kruger, Robert Collins) | 79 | (Marius Kruger, Robert Collins) |
30 | 70 | 80 | ||
31 | 81 | ======= | ||
32 | 82 | * Configuration files in ``${BZR_HOME}`` are now protected | ||
33 | 83 | against concurrent writers. (Vincent Ladeuil, #525571) | ||
34 | 84 | |||
35 | 85 | >>>>>>> MERGE-SOURCE | ||
36 | 71 | * Ensure that wrong path specifications in ``BZR_PLUGINS_AT`` display | 86 | * Ensure that wrong path specifications in ``BZR_PLUGINS_AT`` display |
37 | 72 | proper error messages. (Vincent Ladeuil, #591215) | 87 | proper error messages. (Vincent Ladeuil, #591215) |
38 | 73 | 88 | ||
39 | 74 | 89 | ||
40 | === modified file 'bzrlib/builtins.py' | |||
41 | --- bzrlib/builtins.py 2010-06-23 08:19:28 +0000 | |||
42 | +++ bzrlib/builtins.py 2010-06-30 14:21:34 +0000 | |||
43 | @@ -4830,7 +4830,10 @@ | |||
44 | 4830 | 4830 | ||
45 | 4831 | 4831 | ||
46 | 4832 | class cmd_break_lock(Command): | 4832 | class cmd_break_lock(Command): |
48 | 4833 | __doc__ = """Break a dead lock on a repository, branch or working directory. | 4833 | __doc__ = """Break a dead lock. |
49 | 4834 | |||
50 | 4835 | This command breaks a lock on a repository, branch, working directory or | ||
51 | 4836 | config file. | ||
52 | 4834 | 4837 | ||
53 | 4835 | CAUTION: Locks should only be broken when you are sure that the process | 4838 | CAUTION: Locks should only be broken when you are sure that the process |
54 | 4836 | holding the lock has been stopped. | 4839 | holding the lock has been stopped. |
55 | @@ -4841,17 +4844,27 @@ | |||
56 | 4841 | :Examples: | 4844 | :Examples: |
57 | 4842 | bzr break-lock | 4845 | bzr break-lock |
58 | 4843 | bzr break-lock bzr+ssh://example.com/bzr/foo | 4846 | bzr break-lock bzr+ssh://example.com/bzr/foo |
59 | 4847 | bzr break-lock --conf ~/.bazaar | ||
60 | 4844 | """ | 4848 | """ |
61 | 4849 | |||
62 | 4845 | takes_args = ['location?'] | 4850 | takes_args = ['location?'] |
63 | 4851 | takes_options = [ | ||
64 | 4852 | Option('conf', | ||
65 | 4853 | help='LOCATION is a directory containing configuration files.'), | ||
66 | 4854 | ] | ||
67 | 4846 | 4855 | ||
69 | 4847 | def run(self, location=None, show=False): | 4856 | def run(self, location=None, conf=False): |
70 | 4848 | if location is None: | 4857 | if location is None: |
71 | 4849 | location = u'.' | 4858 | location = u'.' |
77 | 4850 | control, relpath = bzrdir.BzrDir.open_containing(location) | 4859 | if conf: |
78 | 4851 | try: | 4860 | c = config.LockableConfig(lambda : location) |
79 | 4852 | control.break_lock() | 4861 | c.break_lock() |
80 | 4853 | except NotImplementedError: | 4862 | else: |
81 | 4854 | pass | 4863 | control, relpath = bzrdir.BzrDir.open_containing(location) |
82 | 4864 | try: | ||
83 | 4865 | control.break_lock() | ||
84 | 4866 | except NotImplementedError: | ||
85 | 4867 | pass | ||
86 | 4855 | 4868 | ||
87 | 4856 | 4869 | ||
88 | 4857 | class cmd_wait_until_signalled(Command): | 4870 | class cmd_wait_until_signalled(Command): |
89 | 4858 | 4871 | ||
90 | === modified file 'bzrlib/config.py' | |||
91 | --- bzrlib/config.py 2010-06-02 04:50:35 +0000 | |||
92 | +++ bzrlib/config.py 2010-06-30 14:21:34 +0000 | |||
93 | @@ -62,9 +62,11 @@ | |||
94 | 62 | up=pull | 62 | up=pull |
95 | 63 | """ | 63 | """ |
96 | 64 | 64 | ||
97 | 65 | from cStringIO import StringIO | ||
98 | 65 | import os | 66 | import os |
99 | 66 | import sys | 67 | import sys |
100 | 67 | 68 | ||
101 | 69 | from bzrlib.decorators import needs_write_lock | ||
102 | 68 | from bzrlib.lazy_import import lazy_import | 70 | from bzrlib.lazy_import import lazy_import |
103 | 69 | lazy_import(globals(), """ | 71 | lazy_import(globals(), """ |
104 | 70 | import errno | 72 | import errno |
105 | @@ -76,11 +78,14 @@ | |||
106 | 76 | from bzrlib import ( | 78 | from bzrlib import ( |
107 | 77 | debug, | 79 | debug, |
108 | 78 | errors, | 80 | errors, |
109 | 81 | lock, | ||
110 | 82 | lockdir, | ||
111 | 79 | mail_client, | 83 | mail_client, |
112 | 80 | osutils, | 84 | osutils, |
113 | 81 | registry, | 85 | registry, |
114 | 82 | symbol_versioning, | 86 | symbol_versioning, |
115 | 83 | trace, | 87 | trace, |
116 | 88 | transport, | ||
117 | 84 | ui, | 89 | ui, |
118 | 85 | urlutils, | 90 | urlutils, |
119 | 86 | win32utils, | 91 | win32utils, |
120 | @@ -356,18 +361,30 @@ | |||
121 | 356 | self._get_filename = get_filename | 361 | self._get_filename = get_filename |
122 | 357 | self._parser = None | 362 | self._parser = None |
123 | 358 | 363 | ||
125 | 359 | def _get_parser(self, file=None): | 364 | def _get_parser(self, infile=None): |
126 | 360 | if self._parser is not None: | 365 | if self._parser is not None: |
127 | 361 | return self._parser | 366 | return self._parser |
132 | 362 | if file is None: | 367 | # Do we have a file name for the config file |
133 | 363 | input = self._get_filename() | 368 | if self._get_filename is None: |
134 | 364 | else: | 369 | fname = None |
135 | 365 | input = file | 370 | else: |
136 | 371 | fname = self._get_filename() | ||
137 | 372 | # Do we have a content for the config file ? | ||
138 | 373 | if infile is None: | ||
139 | 374 | fname_or_content = fname | ||
140 | 375 | else: | ||
141 | 376 | fname_or_content = infile | ||
142 | 377 | # Build the ConfigObj | ||
143 | 378 | p = None | ||
144 | 366 | try: | 379 | try: |
146 | 367 | self._parser = ConfigObj(input, encoding='utf-8') | 380 | p = ConfigObj(fname_or_content, encoding='utf-8') |
147 | 368 | except configobj.ConfigObjError, e: | 381 | except configobj.ConfigObjError, e: |
148 | 369 | raise errors.ParseConfigError(e.errors, e.config.filename) | 382 | raise errors.ParseConfigError(e.errors, e.config.filename) |
150 | 370 | return self._parser | 383 | if p is not None and fname is not None: |
151 | 384 | # Make sure p.reload() will use the right file name | ||
152 | 385 | p.filename = fname | ||
153 | 386 | self._parser = p | ||
154 | 387 | return p | ||
155 | 371 | 388 | ||
156 | 372 | def _get_matching_sections(self): | 389 | def _get_matching_sections(self): |
157 | 373 | """Return an ordered list of (section_name, extra_path) pairs. | 390 | """Return an ordered list of (section_name, extra_path) pairs. |
158 | @@ -478,27 +495,76 @@ | |||
159 | 478 | return self.get_user_option('nickname') | 495 | return self.get_user_option('nickname') |
160 | 479 | 496 | ||
161 | 480 | def _write_config_file(self): | 497 | def _write_config_file(self): |
163 | 481 | f = file(self._get_filename(), "wb") | 498 | fname = self._get_filename() |
164 | 499 | conf_dir = os.path.dirname(fname) | ||
165 | 500 | ensure_config_dir_exists(conf_dir) | ||
166 | 501 | f = file(fname, "wb") | ||
167 | 482 | try: | 502 | try: |
169 | 483 | osutils.copy_ownership_from_path(f.name) | 503 | osutils.copy_ownership_from_path(fname) |
170 | 484 | self._get_parser().write(f) | 504 | self._get_parser().write(f) |
171 | 485 | finally: | 505 | finally: |
172 | 486 | f.close() | 506 | f.close() |
173 | 487 | 507 | ||
174 | 488 | 508 | ||
176 | 489 | class GlobalConfig(IniBasedConfig): | 509 | class LockableConfig(IniBasedConfig): |
177 | 510 | """A configuration needing explicit locking for access. | ||
178 | 511 | |||
179 | 512 | If several processes try to write the config file, the accesses need to be | ||
180 | 513 | serialized. | ||
181 | 514 | """ | ||
182 | 515 | |||
183 | 516 | lock_name = 'lock' | ||
184 | 517 | |||
185 | 518 | def __init__(self, get_filename): | ||
186 | 519 | super(LockableConfig, self).__init__(get_filename) | ||
187 | 520 | self.dir = osutils.dirname(osutils.safe_unicode(self._get_filename())) | ||
188 | 521 | self.transport = transport.get_transport(self.dir) | ||
189 | 522 | self._lock = None | ||
190 | 523 | |||
191 | 524 | def lock_write(self, token=None): | ||
192 | 525 | if self._lock is None: | ||
193 | 526 | ensure_config_dir_exists(self.dir) | ||
194 | 527 | self._lock = lockdir.LockDir(self.transport, self.lock_name) | ||
195 | 528 | self._lock.lock_write(token) | ||
196 | 529 | return lock.LogicalLockResult(self.unlock) | ||
197 | 530 | |||
198 | 531 | def unlock(self): | ||
199 | 532 | self._lock.unlock() | ||
200 | 533 | |||
201 | 534 | def break_lock(self): | ||
202 | 535 | if self._lock is None: | ||
203 | 536 | self._lock = lockdir.LockDir(self.transport, self.lock_name) | ||
204 | 537 | self._lock.break_lock() | ||
205 | 538 | |||
206 | 539 | def _write_config_file(self): | ||
207 | 540 | if self._lock is None or not self._lock.is_held: | ||
208 | 541 | # NB: if the following exception is raised it probably means a | ||
209 | 542 | # missing @needs_write_lock decorator on one of the callers. | ||
210 | 543 | raise errors.ObjectNotLocked(self) | ||
211 | 544 | fname = self._get_filename() | ||
212 | 545 | f = StringIO() | ||
213 | 546 | p = self._get_parser() | ||
214 | 547 | p.write(f) | ||
215 | 548 | self.transport.put_bytes(os.path.basename(fname), f.getvalue()) | ||
216 | 549 | # Make sure p.reload() will use the right file name | ||
217 | 550 | p.filename = fname | ||
218 | 551 | osutils.copy_ownership_from_path(fname) | ||
219 | 552 | |||
220 | 553 | |||
221 | 554 | class GlobalConfig(LockableConfig): | ||
222 | 490 | """The configuration that should be used for a specific location.""" | 555 | """The configuration that should be used for a specific location.""" |
223 | 491 | 556 | ||
224 | 492 | def get_editor(self): | ||
225 | 493 | return self._get_user_option('editor') | ||
226 | 494 | |||
227 | 495 | def __init__(self): | 557 | def __init__(self): |
228 | 496 | super(GlobalConfig, self).__init__(config_filename) | 558 | super(GlobalConfig, self).__init__(config_filename) |
229 | 497 | 559 | ||
230 | 560 | @needs_write_lock | ||
231 | 498 | def set_user_option(self, option, value): | 561 | def set_user_option(self, option, value): |
232 | 499 | """Save option and its value in the configuration.""" | 562 | """Save option and its value in the configuration.""" |
233 | 500 | self._set_option(option, value, 'DEFAULT') | 563 | self._set_option(option, value, 'DEFAULT') |
234 | 501 | 564 | ||
235 | 565 | def get_editor(self): | ||
236 | 566 | return self._get_user_option('editor') | ||
237 | 567 | |||
238 | 502 | def get_aliases(self): | 568 | def get_aliases(self): |
239 | 503 | """Return the aliases section.""" | 569 | """Return the aliases section.""" |
240 | 504 | if 'ALIASES' in self._get_parser(): | 570 | if 'ALIASES' in self._get_parser(): |
241 | @@ -506,10 +572,12 @@ | |||
242 | 506 | else: | 572 | else: |
243 | 507 | return {} | 573 | return {} |
244 | 508 | 574 | ||
245 | 575 | @needs_write_lock | ||
246 | 509 | def set_alias(self, alias_name, alias_command): | 576 | def set_alias(self, alias_name, alias_command): |
247 | 510 | """Save the alias in the configuration.""" | 577 | """Save the alias in the configuration.""" |
248 | 511 | self._set_option(alias_name, alias_command, 'ALIASES') | 578 | self._set_option(alias_name, alias_command, 'ALIASES') |
249 | 512 | 579 | ||
250 | 580 | @needs_write_lock | ||
251 | 513 | def unset_alias(self, alias_name): | 581 | def unset_alias(self, alias_name): |
252 | 514 | """Unset an existing alias.""" | 582 | """Unset an existing alias.""" |
253 | 515 | aliases = self._get_parser().get('ALIASES') | 583 | aliases = self._get_parser().get('ALIASES') |
254 | @@ -519,15 +587,13 @@ | |||
255 | 519 | self._write_config_file() | 587 | self._write_config_file() |
256 | 520 | 588 | ||
257 | 521 | def _set_option(self, option, value, section): | 589 | def _set_option(self, option, value, section): |
262 | 522 | # FIXME: RBC 20051029 This should refresh the parser and also take a | 590 | if self._parser is not None: |
263 | 523 | # file lock on bazaar.conf. | 591 | self._parser.reload() |
260 | 524 | conf_dir = os.path.dirname(self._get_filename()) | ||
261 | 525 | ensure_config_dir_exists(conf_dir) | ||
264 | 526 | self._get_parser().setdefault(section, {})[option] = value | 592 | self._get_parser().setdefault(section, {})[option] = value |
265 | 527 | self._write_config_file() | 593 | self._write_config_file() |
266 | 528 | 594 | ||
267 | 529 | 595 | ||
269 | 530 | class LocationConfig(IniBasedConfig): | 596 | class LocationConfig(LockableConfig): |
270 | 531 | """A configuration object that gives the policy for a location.""" | 597 | """A configuration object that gives the policy for a location.""" |
271 | 532 | 598 | ||
272 | 533 | def __init__(self, location): | 599 | def __init__(self, location): |
273 | @@ -643,6 +709,7 @@ | |||
274 | 643 | if policy_key in self._get_parser()[section]: | 709 | if policy_key in self._get_parser()[section]: |
275 | 644 | del self._get_parser()[section][policy_key] | 710 | del self._get_parser()[section][policy_key] |
276 | 645 | 711 | ||
277 | 712 | @needs_write_lock | ||
278 | 646 | def set_user_option(self, option, value, store=STORE_LOCATION): | 713 | def set_user_option(self, option, value, store=STORE_LOCATION): |
279 | 647 | """Save option and its value in the configuration.""" | 714 | """Save option and its value in the configuration.""" |
280 | 648 | if store not in [STORE_LOCATION, | 715 | if store not in [STORE_LOCATION, |
281 | @@ -650,10 +717,8 @@ | |||
282 | 650 | STORE_LOCATION_APPENDPATH]: | 717 | STORE_LOCATION_APPENDPATH]: |
283 | 651 | raise ValueError('bad storage policy %r for %r' % | 718 | raise ValueError('bad storage policy %r for %r' % |
284 | 652 | (store, option)) | 719 | (store, option)) |
289 | 653 | # FIXME: RBC 20051029 This should refresh the parser and also take a | 720 | if self._parser is not None: |
290 | 654 | # file lock on locations.conf. | 721 | self._parser.reload() |
287 | 655 | conf_dir = os.path.dirname(self._get_filename()) | ||
288 | 656 | ensure_config_dir_exists(conf_dir) | ||
291 | 657 | location = self.location | 722 | location = self.location |
292 | 658 | if location.endswith('/'): | 723 | if location.endswith('/'): |
293 | 659 | location = location[:-1] | 724 | location = location[:-1] |
294 | 660 | 725 | ||
295 | === modified file 'bzrlib/tests/blackbox/test_break_lock.py' | |||
296 | --- bzrlib/tests/blackbox/test_break_lock.py 2010-06-11 07:32:12 +0000 | |||
297 | +++ bzrlib/tests/blackbox/test_break_lock.py 2010-06-30 14:21:34 +0000 | |||
298 | @@ -18,17 +18,18 @@ | |||
299 | 18 | 18 | ||
300 | 19 | import os | 19 | import os |
301 | 20 | 20 | ||
302 | 21 | import bzrlib | ||
303 | 22 | from bzrlib import ( | 21 | from bzrlib import ( |
304 | 22 | branch, | ||
305 | 23 | bzrdir, | ||
306 | 24 | config, | ||
307 | 23 | errors, | 25 | errors, |
308 | 24 | lockdir, | 26 | lockdir, |
309 | 27 | osutils, | ||
310 | 28 | tests, | ||
311 | 25 | ) | 29 | ) |
318 | 26 | from bzrlib.branch import Branch | 30 | |
319 | 27 | from bzrlib.bzrdir import BzrDir | 31 | |
320 | 28 | from bzrlib.tests import TestCaseWithTransport | 32 | class TestBreakLock(tests.TestCaseWithTransport): |
315 | 29 | |||
316 | 30 | |||
317 | 31 | class TestBreakLock(TestCaseWithTransport): | ||
321 | 32 | 33 | ||
322 | 33 | # General principal for break-lock: All the elements that might be locked | 34 | # General principal for break-lock: All the elements that might be locked |
323 | 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. |
324 | @@ -52,14 +53,14 @@ | |||
325 | 52 | 'repo/', | 53 | 'repo/', |
326 | 53 | 'repo/branch/', | 54 | 'repo/branch/', |
327 | 54 | 'checkout/']) | 55 | 'checkout/']) |
330 | 55 | bzrlib.bzrdir.BzrDir.create('master-repo').create_repository() | 56 | bzrdir.BzrDir.create('master-repo').create_repository() |
331 | 56 | self.master_branch = bzrlib.bzrdir.BzrDir.create_branch_convenience( | 57 | self.master_branch = bzrdir.BzrDir.create_branch_convenience( |
332 | 57 | 'master-repo/master-branch') | 58 | 'master-repo/master-branch') |
335 | 58 | bzrlib.bzrdir.BzrDir.create('repo').create_repository() | 59 | bzrdir.BzrDir.create('repo').create_repository() |
336 | 59 | local_branch = bzrlib.bzrdir.BzrDir.create_branch_convenience('repo/branch') | 60 | local_branch = bzrdir.BzrDir.create_branch_convenience('repo/branch') |
337 | 60 | local_branch.bind(self.master_branch) | 61 | local_branch.bind(self.master_branch) |
340 | 61 | checkoutdir = bzrlib.bzrdir.BzrDir.create('checkout') | 62 | checkoutdir = bzrdir.BzrDir.create('checkout') |
341 | 62 | bzrlib.branch.BranchReferenceFormat().initialize( | 63 | branch.BranchReferenceFormat().initialize( |
342 | 63 | checkoutdir, target_branch=local_branch) | 64 | checkoutdir, target_branch=local_branch) |
343 | 64 | self.wt = checkoutdir.create_workingtree() | 65 | self.wt = checkoutdir.create_workingtree() |
344 | 65 | 66 | ||
345 | @@ -73,7 +74,7 @@ | |||
346 | 73 | # however, we dont test breaking the working tree because we | 74 | # however, we dont test breaking the working tree because we |
347 | 74 | # cannot accurately do so right now: the dirstate lock is held | 75 | # cannot accurately do so right now: the dirstate lock is held |
348 | 75 | # by an os lock, and we need to spawn a separate process to lock it | 76 | # by an os lock, and we need to spawn a separate process to lock it |
350 | 76 | # thne kill -9 it. | 77 | # then kill -9 it. |
351 | 77 | # sketch of test: | 78 | # sketch of test: |
352 | 78 | # lock most of the dir: | 79 | # lock most of the dir: |
353 | 79 | self.wt.branch.lock_write() | 80 | self.wt.branch.lock_write() |
354 | @@ -82,22 +83,45 @@ | |||
355 | 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. |
356 | 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") |
357 | 84 | # a new tree instance should be lockable | 85 | # a new tree instance should be lockable |
361 | 85 | branch = bzrlib.branch.Branch.open('checkout') | 86 | b = branch.Branch.open('checkout') |
362 | 86 | branch.lock_write() | 87 | b.lock_write() |
363 | 87 | branch.unlock() | 88 | b.unlock() |
364 | 88 | # and a new instance of the master branch | 89 | # and a new instance of the master branch |
366 | 89 | mb = branch.get_master_branch() | 90 | mb = b.get_master_branch() |
367 | 90 | mb.lock_write() | 91 | mb.lock_write() |
368 | 91 | mb.unlock() | 92 | mb.unlock() |
369 | 92 | self.assertRaises(errors.LockBroken, self.wt.unlock) | 93 | self.assertRaises(errors.LockBroken, self.wt.unlock) |
370 | 93 | self.assertRaises(errors.LockBroken, self.master_branch.unlock) | 94 | self.assertRaises(errors.LockBroken, self.master_branch.unlock) |
371 | 94 | 95 | ||
372 | 95 | 96 | ||
374 | 96 | class TestBreakLockOldBranch(TestCaseWithTransport): | 97 | class TestBreakLockOldBranch(tests.TestCaseWithTransport): |
375 | 97 | 98 | ||
376 | 98 | def test_break_lock_format_5_bzrdir(self): | 99 | def test_break_lock_format_5_bzrdir(self): |
377 | 99 | # break lock on a format 5 bzrdir should just return | 100 | # break lock on a format 5 bzrdir should just return |
379 | 100 | self.make_branch_and_tree('foo', format=bzrlib.bzrdir.BzrDirFormat5()) | 101 | self.make_branch_and_tree('foo', format=bzrdir.BzrDirFormat5()) |
380 | 101 | out, err = self.run_bzr('break-lock foo') | 102 | out, err = self.run_bzr('break-lock foo') |
381 | 102 | self.assertEqual('', out) | 103 | self.assertEqual('', out) |
382 | 103 | self.assertEqual('', err) | 104 | self.assertEqual('', err) |
383 | 105 | |||
384 | 106 | |||
385 | 107 | class TestConfigBreakLock(tests.TestCaseWithTransport): | ||
386 | 108 | |||
387 | 109 | def create_pending_lock(self): | ||
388 | 110 | def config_file_name(): | ||
389 | 111 | return './my.conf' | ||
390 | 112 | self.build_tree_contents([(config_file_name(), '[DEFAULT]\none=1\n')]) | ||
391 | 113 | conf = config.LockableConfig(config_file_name) | ||
392 | 114 | conf.lock_write() | ||
393 | 115 | return conf | ||
394 | 116 | |||
395 | 117 | def test_create_pending_lock(self): | ||
396 | 118 | conf = self.create_pending_lock() | ||
397 | 119 | self.addCleanup(conf.unlock) | ||
398 | 120 | self.assertTrue(conf._lock.is_held) | ||
399 | 121 | |||
400 | 122 | def test_break_lock(self): | ||
401 | 123 | conf = self.create_pending_lock() | ||
402 | 124 | self.run_bzr('break-lock --conf %s' | ||
403 | 125 | % osutils.dirname(conf._get_filename()), | ||
404 | 126 | stdin="y\n") | ||
405 | 127 | self.assertRaises(errors.LockBroken, conf.unlock) | ||
406 | 104 | 128 | ||
407 | === modified file 'bzrlib/tests/test_commands.py' | |||
408 | --- bzrlib/tests/test_commands.py 2010-05-27 21:16:48 +0000 | |||
409 | +++ bzrlib/tests/test_commands.py 2010-06-30 14:21:34 +0000 | |||
410 | @@ -97,7 +97,7 @@ | |||
411 | 97 | def _get_config(self, config_text): | 97 | def _get_config(self, config_text): |
412 | 98 | my_config = config.GlobalConfig() | 98 | my_config = config.GlobalConfig() |
413 | 99 | config_file = StringIO(config_text.encode('utf-8')) | 99 | config_file = StringIO(config_text.encode('utf-8')) |
415 | 100 | my_config._parser = my_config._get_parser(file=config_file) | 100 | my_config._parser = my_config._get_parser(infile=config_file) |
416 | 101 | return my_config | 101 | return my_config |
417 | 102 | 102 | ||
418 | 103 | def test_simple(self): | 103 | def test_simple(self): |
419 | 104 | 104 | ||
420 | === modified file 'bzrlib/tests/test_config.py' | |||
421 | --- bzrlib/tests/test_config.py 2010-04-23 07:15:23 +0000 | |||
422 | +++ bzrlib/tests/test_config.py 2010-06-30 14:21:34 +0000 | |||
423 | @@ -19,6 +19,7 @@ | |||
424 | 19 | from cStringIO import StringIO | 19 | from cStringIO import StringIO |
425 | 20 | import os | 20 | import os |
426 | 21 | import sys | 21 | import sys |
427 | 22 | import threading | ||
428 | 22 | 23 | ||
429 | 23 | #import bzrlib specific imports here | 24 | #import bzrlib specific imports here |
430 | 24 | from bzrlib import ( | 25 | from bzrlib import ( |
431 | @@ -38,6 +39,32 @@ | |||
432 | 38 | from bzrlib.util.configobj import configobj | 39 | from bzrlib.util.configobj import configobj |
433 | 39 | 40 | ||
434 | 40 | 41 | ||
435 | 42 | def lockable_config_scenarios(): | ||
436 | 43 | return [ | ||
437 | 44 | ('global', | ||
438 | 45 | {'config_file_name': config.config_filename, | ||
439 | 46 | 'config_class': config.GlobalConfig, | ||
440 | 47 | 'config_args': [], | ||
441 | 48 | 'config_section': 'DEFAULT'}), | ||
442 | 49 | ('locations', | ||
443 | 50 | {'config_file_name': config.locations_config_filename, | ||
444 | 51 | 'config_class': config.LocationConfig, | ||
445 | 52 | 'config_args': ['.'], | ||
446 | 53 | 'config_section': '.'}),] | ||
447 | 54 | |||
448 | 55 | |||
449 | 56 | def load_tests(standard_tests, module, loader): | ||
450 | 57 | suite = loader.suiteClass() | ||
451 | 58 | |||
452 | 59 | lc_tests, remaining_tests = tests.split_suite_by_condition( | ||
453 | 60 | standard_tests, tests.condition_isinstance(( | ||
454 | 61 | TestLockableConfig, | ||
455 | 62 | ))) | ||
456 | 63 | tests.multiply_tests(lc_tests, lockable_config_scenarios(), suite) | ||
457 | 64 | suite.addTest(remaining_tests) | ||
458 | 65 | return suite | ||
459 | 66 | |||
460 | 67 | |||
461 | 41 | sample_long_alias="log -r-15..-1 --line" | 68 | sample_long_alias="log -r-15..-1 --line" |
462 | 42 | sample_config_text = u""" | 69 | sample_config_text = u""" |
463 | 43 | [DEFAULT] | 70 | [DEFAULT] |
464 | @@ -129,6 +156,9 @@ | |||
465 | 129 | self._calls.append(('keys',)) | 156 | self._calls.append(('keys',)) |
466 | 130 | return [] | 157 | return [] |
467 | 131 | 158 | ||
468 | 159 | def reload(self): | ||
469 | 160 | self._calls.append(('reload',)) | ||
470 | 161 | |||
471 | 132 | def write(self, arg): | 162 | def write(self, arg): |
472 | 133 | self._calls.append(('write',)) | 163 | self._calls.append(('write',)) |
473 | 134 | 164 | ||
474 | @@ -371,7 +401,7 @@ | |||
475 | 371 | 401 | ||
476 | 372 | def make_config_parser(self, s): | 402 | def make_config_parser(self, s): |
477 | 373 | conf = config.IniBasedConfig(None) | 403 | conf = config.IniBasedConfig(None) |
479 | 374 | parser = conf._get_parser(file=StringIO(s.encode('utf-8'))) | 404 | parser = conf._get_parser(infile=StringIO(s.encode('utf-8'))) |
480 | 375 | return conf, parser | 405 | return conf, parser |
481 | 376 | 406 | ||
482 | 377 | 407 | ||
483 | @@ -384,16 +414,144 @@ | |||
484 | 384 | config_file = StringIO(sample_config_text.encode('utf-8')) | 414 | config_file = StringIO(sample_config_text.encode('utf-8')) |
485 | 385 | my_config = config.IniBasedConfig(None) | 415 | my_config = config.IniBasedConfig(None) |
486 | 386 | self.failUnless( | 416 | self.failUnless( |
488 | 387 | isinstance(my_config._get_parser(file=config_file), | 417 | isinstance(my_config._get_parser(infile=config_file), |
489 | 388 | configobj.ConfigObj)) | 418 | configobj.ConfigObj)) |
490 | 389 | 419 | ||
491 | 390 | def test_cached(self): | 420 | def test_cached(self): |
492 | 391 | config_file = StringIO(sample_config_text.encode('utf-8')) | 421 | config_file = StringIO(sample_config_text.encode('utf-8')) |
493 | 392 | my_config = config.IniBasedConfig(None) | 422 | my_config = config.IniBasedConfig(None) |
495 | 393 | parser = my_config._get_parser(file=config_file) | 423 | parser = my_config._get_parser(infile=config_file) |
496 | 394 | self.failUnless(my_config._get_parser() is parser) | 424 | self.failUnless(my_config._get_parser() is parser) |
497 | 395 | 425 | ||
498 | 396 | 426 | ||
499 | 427 | class TestLockableConfig(tests.TestCaseInTempDir): | ||
500 | 428 | |||
501 | 429 | # Set by load_tests | ||
502 | 430 | config_file_name = None | ||
503 | 431 | config_class = None | ||
504 | 432 | config_args = None | ||
505 | 433 | config_section = None | ||
506 | 434 | |||
507 | 435 | def setUp(self): | ||
508 | 436 | super(TestLockableConfig, self).setUp() | ||
509 | 437 | config.ensure_config_dir_exists() | ||
510 | 438 | text = '[%s]\none=1\ntwo=2\n' % (self.config_section,) | ||
511 | 439 | self.build_tree_contents([(self.config_file_name(), text)]) | ||
512 | 440 | |||
513 | 441 | def create_config(self): | ||
514 | 442 | return self.config_class(*self.config_args) | ||
515 | 443 | |||
516 | 444 | def test_simple_read_access(self): | ||
517 | 445 | c = self.create_config() | ||
518 | 446 | self.assertEquals('1', c.get_user_option('one')) | ||
519 | 447 | |||
520 | 448 | def test_simple_write_access(self): | ||
521 | 449 | c = self.create_config() | ||
522 | 450 | c.set_user_option('one', 'one') | ||
523 | 451 | self.assertEquals('one', c.get_user_option('one')) | ||
524 | 452 | |||
525 | 453 | def test_unlocked_config(self): | ||
526 | 454 | c = self.create_config() | ||
527 | 455 | self.assertRaises(errors.ObjectNotLocked, c._write_config_file) | ||
528 | 456 | |||
529 | 457 | def test_listen_to_the_last_speaker(self): | ||
530 | 458 | c1 = self.create_config() | ||
531 | 459 | c2 = self.create_config() | ||
532 | 460 | c1.set_user_option('one', 'ONE') | ||
533 | 461 | c2.set_user_option('two', 'TWO') | ||
534 | 462 | self.assertEquals('ONE', c1.get_user_option('one')) | ||
535 | 463 | self.assertEquals('TWO', c2.get_user_option('two')) | ||
536 | 464 | # The second update respect the first one | ||
537 | 465 | self.assertEquals('ONE', c2.get_user_option('one')) | ||
538 | 466 | |||
539 | 467 | def test_last_speaker_wins(self): | ||
540 | 468 | # If the same config is not shared, the same variable modified twice | ||
541 | 469 | # can only see a single result. | ||
542 | 470 | c1 = self.create_config() | ||
543 | 471 | c2 = self.create_config() | ||
544 | 472 | c1.set_user_option('one', 'c1') | ||
545 | 473 | c2.set_user_option('one', 'c2') | ||
546 | 474 | self.assertEquals('c2', c2.get_user_option('one')) | ||
547 | 475 | # The first modification is still available until another refresh | ||
548 | 476 | # occur | ||
549 | 477 | self.assertEquals('c1', c1.get_user_option('one')) | ||
550 | 478 | c1.set_user_option('two', 'done') | ||
551 | 479 | self.assertEquals('c2', c1.get_user_option('one')) | ||
552 | 480 | |||
553 | 481 | def test_writes_are_serialized(self): | ||
554 | 482 | c1 = self.create_config() | ||
555 | 483 | c2 = self.create_config() | ||
556 | 484 | |||
557 | 485 | # We spawn a thread that will pause *during* the write | ||
558 | 486 | before_writing = threading.Event() | ||
559 | 487 | after_writing = threading.Event() | ||
560 | 488 | writing_done = threading.Event() | ||
561 | 489 | c1_orig = c1._write_config_file | ||
562 | 490 | def c1_write_config_file(): | ||
563 | 491 | before_writing.set() | ||
564 | 492 | c1_orig() | ||
565 | 493 | # The lock is held we wait for the main thread to decide when to | ||
566 | 494 | # continue | ||
567 | 495 | after_writing.wait() | ||
568 | 496 | c1._write_config_file = c1_write_config_file | ||
569 | 497 | def c1_set_option(): | ||
570 | 498 | c1.set_user_option('one', 'c1') | ||
571 | 499 | writing_done.set() | ||
572 | 500 | t1 = threading.Thread(target=c1_set_option) | ||
573 | 501 | # Collect the thread after the test | ||
574 | 502 | self.addCleanup(t1.join) | ||
575 | 503 | # Be ready to unblock the thread if the test goes wrong | ||
576 | 504 | self.addCleanup(after_writing.set) | ||
577 | 505 | t1.start() | ||
578 | 506 | before_writing.wait() | ||
579 | 507 | self.assertTrue(c1._lock.is_held) | ||
580 | 508 | self.assertRaises(errors.LockContention, | ||
581 | 509 | c2.set_user_option, 'one', 'c2') | ||
582 | 510 | self.assertEquals('c1', c1.get_user_option('one')) | ||
583 | 511 | # Let the lock be released | ||
584 | 512 | after_writing.set() | ||
585 | 513 | writing_done.wait() | ||
586 | 514 | c2.set_user_option('one', 'c2') | ||
587 | 515 | self.assertEquals('c2', c2.get_user_option('one')) | ||
588 | 516 | |||
589 | 517 | def test_read_while_writing(self): | ||
590 | 518 | c1 = self.create_config() | ||
591 | 519 | # We spawn a thread that will pause *during* the write | ||
592 | 520 | ready_to_write = threading.Event() | ||
593 | 521 | do_writing = threading.Event() | ||
594 | 522 | writing_done = threading.Event() | ||
595 | 523 | c1_orig = c1._write_config_file | ||
596 | 524 | def c1_write_config_file(): | ||
597 | 525 | ready_to_write.set() | ||
598 | 526 | # The lock is held we wait for the main thread to decide when to | ||
599 | 527 | # continue | ||
600 | 528 | do_writing.wait() | ||
601 | 529 | c1_orig() | ||
602 | 530 | writing_done.set() | ||
603 | 531 | c1._write_config_file = c1_write_config_file | ||
604 | 532 | def c1_set_option(): | ||
605 | 533 | c1.set_user_option('one', 'c1') | ||
606 | 534 | t1 = threading.Thread(target=c1_set_option) | ||
607 | 535 | # Collect the thread after the test | ||
608 | 536 | self.addCleanup(t1.join) | ||
609 | 537 | # Be ready to unblock the thread if the test goes wrong | ||
610 | 538 | self.addCleanup(do_writing.set) | ||
611 | 539 | t1.start() | ||
612 | 540 | # Ensure the thread is ready to write | ||
613 | 541 | ready_to_write.wait() | ||
614 | 542 | self.assertTrue(c1._lock.is_held) | ||
615 | 543 | self.assertEquals('c1', c1.get_user_option('one')) | ||
616 | 544 | # If we read during the write, we get the old value | ||
617 | 545 | c2 = self.create_config() | ||
618 | 546 | self.assertEquals('1', c2.get_user_option('one')) | ||
619 | 547 | # Let the writing occur and ensure it occurred | ||
620 | 548 | do_writing.set() | ||
621 | 549 | writing_done.wait() | ||
622 | 550 | # Now we get the updated value | ||
623 | 551 | c3 = self.create_config() | ||
624 | 552 | self.assertEquals('c1', c3.get_user_option('one')) | ||
625 | 553 | |||
626 | 554 | |||
627 | 397 | class TestGetUserOptionAs(TestIniConfig): | 555 | class TestGetUserOptionAs(TestIniConfig): |
628 | 398 | 556 | ||
629 | 399 | def test_get_user_option_as_bool(self): | 557 | def test_get_user_option_as_bool(self): |
630 | @@ -583,26 +741,26 @@ | |||
631 | 583 | def test_user_id(self): | 741 | def test_user_id(self): |
632 | 584 | config_file = StringIO(sample_config_text.encode('utf-8')) | 742 | config_file = StringIO(sample_config_text.encode('utf-8')) |
633 | 585 | my_config = config.GlobalConfig() | 743 | my_config = config.GlobalConfig() |
635 | 586 | my_config._parser = my_config._get_parser(file=config_file) | 744 | my_config._parser = my_config._get_parser(infile=config_file) |
636 | 587 | self.assertEqual(u"Erik B\u00e5gfors <erik@bagfors.nu>", | 745 | self.assertEqual(u"Erik B\u00e5gfors <erik@bagfors.nu>", |
637 | 588 | my_config._get_user_id()) | 746 | my_config._get_user_id()) |
638 | 589 | 747 | ||
639 | 590 | def test_absent_user_id(self): | 748 | def test_absent_user_id(self): |
640 | 591 | config_file = StringIO("") | 749 | config_file = StringIO("") |
641 | 592 | my_config = config.GlobalConfig() | 750 | my_config = config.GlobalConfig() |
643 | 593 | my_config._parser = my_config._get_parser(file=config_file) | 751 | my_config._parser = my_config._get_parser(infile=config_file) |
644 | 594 | self.assertEqual(None, my_config._get_user_id()) | 752 | self.assertEqual(None, my_config._get_user_id()) |
645 | 595 | 753 | ||
646 | 596 | def test_configured_editor(self): | 754 | def test_configured_editor(self): |
647 | 597 | config_file = StringIO(sample_config_text.encode('utf-8')) | 755 | config_file = StringIO(sample_config_text.encode('utf-8')) |
648 | 598 | my_config = config.GlobalConfig() | 756 | my_config = config.GlobalConfig() |
650 | 599 | my_config._parser = my_config._get_parser(file=config_file) | 757 | my_config._parser = my_config._get_parser(infile=config_file) |
651 | 600 | self.assertEqual("vim", my_config.get_editor()) | 758 | self.assertEqual("vim", my_config.get_editor()) |
652 | 601 | 759 | ||
653 | 602 | def test_signatures_always(self): | 760 | def test_signatures_always(self): |
654 | 603 | config_file = StringIO(sample_always_signatures) | 761 | config_file = StringIO(sample_always_signatures) |
655 | 604 | my_config = config.GlobalConfig() | 762 | my_config = config.GlobalConfig() |
657 | 605 | my_config._parser = my_config._get_parser(file=config_file) | 763 | my_config._parser = my_config._get_parser(infile=config_file) |
658 | 606 | self.assertEqual(config.CHECK_NEVER, | 764 | self.assertEqual(config.CHECK_NEVER, |
659 | 607 | my_config.signature_checking()) | 765 | my_config.signature_checking()) |
660 | 608 | self.assertEqual(config.SIGN_ALWAYS, | 766 | self.assertEqual(config.SIGN_ALWAYS, |
661 | @@ -612,7 +770,7 @@ | |||
662 | 612 | def test_signatures_if_possible(self): | 770 | def test_signatures_if_possible(self): |
663 | 613 | config_file = StringIO(sample_maybe_signatures) | 771 | config_file = StringIO(sample_maybe_signatures) |
664 | 614 | my_config = config.GlobalConfig() | 772 | my_config = config.GlobalConfig() |
666 | 615 | my_config._parser = my_config._get_parser(file=config_file) | 773 | my_config._parser = my_config._get_parser(infile=config_file) |
667 | 616 | self.assertEqual(config.CHECK_NEVER, | 774 | self.assertEqual(config.CHECK_NEVER, |
668 | 617 | my_config.signature_checking()) | 775 | my_config.signature_checking()) |
669 | 618 | self.assertEqual(config.SIGN_WHEN_REQUIRED, | 776 | self.assertEqual(config.SIGN_WHEN_REQUIRED, |
670 | @@ -622,7 +780,7 @@ | |||
671 | 622 | def test_signatures_ignore(self): | 780 | def test_signatures_ignore(self): |
672 | 623 | config_file = StringIO(sample_ignore_signatures) | 781 | config_file = StringIO(sample_ignore_signatures) |
673 | 624 | my_config = config.GlobalConfig() | 782 | my_config = config.GlobalConfig() |
675 | 625 | my_config._parser = my_config._get_parser(file=config_file) | 783 | my_config._parser = my_config._get_parser(infile=config_file) |
676 | 626 | self.assertEqual(config.CHECK_ALWAYS, | 784 | self.assertEqual(config.CHECK_ALWAYS, |
677 | 627 | my_config.signature_checking()) | 785 | my_config.signature_checking()) |
678 | 628 | self.assertEqual(config.SIGN_NEVER, | 786 | self.assertEqual(config.SIGN_NEVER, |
679 | @@ -632,7 +790,7 @@ | |||
680 | 632 | def _get_sample_config(self): | 790 | def _get_sample_config(self): |
681 | 633 | config_file = StringIO(sample_config_text.encode('utf-8')) | 791 | config_file = StringIO(sample_config_text.encode('utf-8')) |
682 | 634 | my_config = config.GlobalConfig() | 792 | my_config = config.GlobalConfig() |
684 | 635 | my_config._parser = my_config._get_parser(file=config_file) | 793 | my_config._parser = my_config._get_parser(infile=config_file) |
685 | 636 | return my_config | 794 | return my_config |
686 | 637 | 795 | ||
687 | 638 | def test_gpg_signing_command(self): | 796 | def test_gpg_signing_command(self): |
688 | @@ -643,7 +801,7 @@ | |||
689 | 643 | def _get_empty_config(self): | 801 | def _get_empty_config(self): |
690 | 644 | config_file = StringIO("") | 802 | config_file = StringIO("") |
691 | 645 | my_config = config.GlobalConfig() | 803 | my_config = config.GlobalConfig() |
693 | 646 | my_config._parser = my_config._get_parser(file=config_file) | 804 | my_config._parser = my_config._get_parser(infile=config_file) |
694 | 647 | return my_config | 805 | return my_config |
695 | 648 | 806 | ||
696 | 649 | def test_gpg_signing_command_unset(self): | 807 | def test_gpg_signing_command_unset(self): |
697 | @@ -745,10 +903,11 @@ | |||
698 | 745 | [('__init__', config.locations_config_filename(), | 903 | [('__init__', config.locations_config_filename(), |
699 | 746 | 'utf-8')]) | 904 | 'utf-8')]) |
700 | 747 | config.ensure_config_dir_exists() | 905 | config.ensure_config_dir_exists() |
701 | 748 | #os.mkdir(config.config_dir()) | ||
702 | 749 | f = file(config.branches_config_filename(), 'wb') | 906 | f = file(config.branches_config_filename(), 'wb') |
705 | 750 | f.write('') | 907 | try: |
706 | 751 | f.close() | 908 | f.write('') |
707 | 909 | finally: | ||
708 | 910 | f.close() | ||
709 | 752 | oldparserclass = config.ConfigObj | 911 | oldparserclass = config.ConfigObj |
710 | 753 | config.ConfigObj = InstrumentedConfigObj | 912 | config.ConfigObj = InstrumentedConfigObj |
711 | 754 | try: | 913 | try: |
712 | @@ -995,10 +1154,15 @@ | |||
713 | 995 | else: | 1154 | else: |
714 | 996 | global_file = StringIO(global_config.encode('utf-8')) | 1155 | global_file = StringIO(global_config.encode('utf-8')) |
715 | 997 | branches_file = StringIO(sample_branches_text.encode('utf-8')) | 1156 | branches_file = StringIO(sample_branches_text.encode('utf-8')) |
716 | 1157 | # Make sure the locations config can be reloaded | ||
717 | 1158 | config.ensure_config_dir_exists() | ||
718 | 1159 | f = file(config.locations_config_filename(), 'wb') | ||
719 | 1160 | try: | ||
720 | 1161 | f.write(branches_file.getvalue()) | ||
721 | 1162 | finally: | ||
722 | 1163 | f.close | ||
723 | 998 | self.my_config = config.BranchConfig(FakeBranch(location)) | 1164 | self.my_config = config.BranchConfig(FakeBranch(location)) |
724 | 999 | # Force location config to use specified file | ||
725 | 1000 | self.my_location_config = self.my_config._get_location_config() | 1165 | self.my_location_config = self.my_config._get_location_config() |
726 | 1001 | self.my_location_config._get_parser(branches_file) | ||
727 | 1002 | # Force global config to use specified file | 1166 | # Force global config to use specified file |
728 | 1003 | self.my_config._get_global_config()._get_parser(global_file) | 1167 | self.my_config._get_global_config()._get_parser(global_file) |
729 | 1004 | 1168 | ||
730 | @@ -1007,25 +1171,14 @@ | |||
731 | 1007 | record = InstrumentedConfigObj("foo") | 1171 | record = InstrumentedConfigObj("foo") |
732 | 1008 | self.my_location_config._parser = record | 1172 | self.my_location_config._parser = record |
733 | 1009 | 1173 | ||
753 | 1010 | real_mkdir = os.mkdir | 1174 | self.callDeprecated(['The recurse option is deprecated as of ' |
754 | 1011 | self.created = False | 1175 | '0.14. The section "/a/c" has been ' |
755 | 1012 | def checked_mkdir(path, mode=0777): | 1176 | 'converted to use policies.'], |
756 | 1013 | self.log('making directory: %s', path) | 1177 | self.my_config.set_user_option, |
757 | 1014 | real_mkdir(path, mode) | 1178 | 'foo', 'bar', store=config.STORE_LOCATION) |
758 | 1015 | self.created = True | 1179 | |
759 | 1016 | 1180 | self.assertEqual([('reload',), | |
760 | 1017 | os.mkdir = checked_mkdir | 1181 | ('__contains__', '/a/c'), |
742 | 1018 | try: | ||
743 | 1019 | self.callDeprecated(['The recurse option is deprecated as of ' | ||
744 | 1020 | '0.14. The section "/a/c" has been ' | ||
745 | 1021 | 'converted to use policies.'], | ||
746 | 1022 | self.my_config.set_user_option, | ||
747 | 1023 | 'foo', 'bar', store=config.STORE_LOCATION) | ||
748 | 1024 | finally: | ||
749 | 1025 | os.mkdir = real_mkdir | ||
750 | 1026 | |||
751 | 1027 | self.failUnless(self.created, 'Failed to create ~/.bazaar') | ||
752 | 1028 | self.assertEqual([('__contains__', '/a/c'), | ||
761 | 1029 | ('__contains__', '/a/c/'), | 1182 | ('__contains__', '/a/c/'), |
762 | 1030 | ('__setitem__', '/a/c', {}), | 1183 | ('__setitem__', '/a/c', {}), |
763 | 1031 | ('__getitem__', '/a/c'), | 1184 | ('__getitem__', '/a/c'), |
764 | @@ -1083,10 +1236,13 @@ | |||
765 | 1083 | if global_config is not None: | 1236 | if global_config is not None: |
766 | 1084 | global_file = StringIO(global_config.encode('utf-8')) | 1237 | global_file = StringIO(global_config.encode('utf-8')) |
767 | 1085 | my_config._get_global_config()._get_parser(global_file) | 1238 | my_config._get_global_config()._get_parser(global_file) |
769 | 1086 | self.my_location_config = my_config._get_location_config() | 1239 | lconf = my_config._get_location_config() |
770 | 1087 | if location_config is not None: | 1240 | if location_config is not None: |
771 | 1088 | location_file = StringIO(location_config.encode('utf-8')) | 1241 | location_file = StringIO(location_config.encode('utf-8')) |
773 | 1089 | self.my_location_config._get_parser(location_file) | 1242 | lconf._get_parser(location_file) |
774 | 1243 | # Make sure the config can be reloaded | ||
775 | 1244 | lconf._parser.filename = config.locations_config_filename() | ||
776 | 1245 | self.my_location_config = lconf | ||
777 | 1090 | if branch_data_config is not None: | 1246 | if branch_data_config is not None: |
778 | 1091 | my_config.branch.control_files.files['branch.conf'] = \ | 1247 | my_config.branch.control_files.files['branch.conf'] = \ |
779 | 1092 | branch_data_config | 1248 | branch_data_config |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Vincent Ladeuil wrote: /bugs.launchpad .net/bugs/ 525571 config. LockableConfig that plugins authors using config files in ~/.bazaar may want to inherit (as LocationConfig and GlobalConfig do now).
> 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:/
>
>
> 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.
>
> 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 enigmail. mozdev. org/
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAkw qMNkACgkQJdeBCY SNAAO7WACfYZOte 5LfqA4Ro4J6U/ 3ZA2Cf tQjcgx047AYI0sJ MiZlfY
ZhkAoLy3d0+
=mMJj
-----END PGP SIGNATURE-----