Merge lp:~vila/bzr/config-lock-remote into lp:bzr
- config-lock-remote
- Merge into bzr.dev
Status: | Merged |
---|---|
Approved by: | Martin Packman |
Approved revision: | no longer in the source branch. |
Merged at revision: | 5943 |
Proposed branch: | lp:~vila/bzr/config-lock-remote |
Merge into: | lp:bzr |
Prerequisite: | lp:~vila/bzr/config-lock-branch |
Diff against target: |
294 lines (+141/-27) 2 files modified
bzrlib/config.py (+31/-8) bzrlib/tests/test_config.py (+110/-19) |
To merge this branch: | bzr merge lp:~vila/bzr/config-lock-remote |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Pool | Needs Information | ||
John A Meinel | Approve | ||
Review via email: mp+62418@code.launchpad.net |
Commit message
Support config remote branch config file.
Description of the change
This add supports for the remote branch.conf files.
It's an incremental improvement, compatible with all servers since it relies on the vfs layer only.
I plan to add a smart verb only when I switch to the new lock scheme (single read/write for a given config file for a given bzr operation).
Martin Pool (mbp) wrote : | # |
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 05/26/2011 08:33 AM, Vincent Ladeuil wrote:
> Vincent Ladeuil has proposed merging lp:~vila/bzr/config-lock-remote into lp:bzr with lp:~vila/bzr/config-lock-branch as a prerequisite.
>
> Requested reviews:
> bzr-core (bzr-core)
>
> For more details, see:
> https:/
>
> This add supports for the remote branch.conf files.
>
> It's an incremental improvement, compatible with all servers since it relies on the vfs layer only.
>
> I plan to add a smart verb only when I switch to the new lock scheme (single read/write for a given config file for a given bzr operation).
>
>
def lock_write(self, token=None):
- - return self.branch.
+ return self.branch_
...
def unlock(self):
- - return self.branch.
+ return self.branch_
^- If you are using a weak_ref you need to handle when branch_ref()
returns None. (because the branch has gone away)
I realize this shouldn't generally happen (I assume the branch is
holding a direct reference to the config object), but it is something
you should at least be prepared for.
Otherwise this just looks like some minor tweaking to the existing code,
which seems fine to me.
John
=:->
review: approve
(Pending responses to Martin's questions.)
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk3
FLAAoJPXZA6Urek
=a0ub
-----END PGP SIGNATURE-----
Vincent Ladeuil (vila) wrote : | # |
> How does that work?
Pretty well :)
Jokes aside, it works *now* because the tests are holding a reference to the branch object while the config object is alive.
Once we start making this object an attribute of the branch, we'll also get the guarantee that the branch is always alive while we use the config object.
Note this pattern is already used in our code base in SmartClientHTTP
Vincent Ladeuil (vila) wrote : | # |
> ^- If you are using a weak_ref you need to handle when branch_ref()
> returns None. (because the branch has gone away)
Having encountered the issue while implementing the associated tests, I know it's handled: an exception is raised making it obvious that the branch is dead. Since this reveals a bug in the code I don't plan to explicitly test for it (since the gc is involved it's inherently racy).
> I realize this shouldn't generally happen (I assume the branch is
> holding a direct reference to the config object),
Not yet, but that's the plan yes.
> but it is something
> you should at least be prepared for.
See above, I didn't have to think twice to understand what was happening, really.
> review: approve
Thanks, the pre-requisite branches are still pending though.
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 05/26/2011 09:41 AM, Vincent Ladeuil wrote:
>> ^- If you are using a weak_ref you need to handle when branch_ref()
>> returns None. (because the branch has gone away)
>
> Having encountered the issue while implementing the associated tests, I know it's handled: an exception is raised making it obvious that the branch is dead. Since this reveals a bug in the code I don't plan to explicitly test for it (since the gc is involved it's inherently racy).
>
>> I realize this shouldn't generally happen (I assume the branch is
>> holding a direct reference to the config object),
>
> Not yet, but that's the plan yes.
>
>> but it is something
>> you should at least be prepared for.
>
> See above, I didn't have to think twice to understand what was happening, really.
>
>> review: approve
>
> Thanks, the pre-requisite branches are still pending though.
>
Getting at AttributeError is an ugly way to say the branch has
disappeared. Very hard to discover when it happens in production
(accidentally certainly, but still possible).
So please consider giving us a nicer error for discoverability purposes.
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk3
su0An10Hfy8GOpv
=Bqhx
-----END PGP SIGNATURE-----
Vincent Ladeuil (vila) wrote : | # |
>>>>> John Arbash Meinel writes:
<snip/>
> Getting at AttributeError is an ugly way to say the branch has
> disappeared. Very hard to discover when it happens in production
> (accidentally certainly, but still possible).
> So please consider giving us a nicer error for discoverability purposes.
Could you point me where in the *actual* code this idiom is used and
such a reporting is taken into account ?
I find it highly unproductive to be blocked from landing incremental
patches when my intent is to ease review.
This code is *not* used in production right now and when it will this
case will never be triggered (and could not possibly be). The only cases
now and tomorrow are unit tests which purposefully *avoid* creating such
objects *without* a protecting branch. And these tests already have a
comment explaining the issue.
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 05/26/2011 12:10 PM, Vincent Ladeuil wrote:
>>>>>> John Arbash Meinel writes:
>
> <snip/>
>
> > Getting at AttributeError is an ugly way to say the branch has
> > disappeared. Very hard to discover when it happens in production
> > (accidentally certainly, but still possible).
>
> > So please consider giving us a nicer error for discoverability purposes.
>
> Could you point me where in the *actual* code this idiom is used and
> such a reporting is taken into account ?
>
> I find it highly unproductive to be blocked from landing incremental
> patches when my intent is to ease review.
>
> This code is *not* used in production right now and when it will this
> case will never be triggered (and could not possibly be). The only cases
> now and tomorrow are unit tests which purposefully *avoid* creating such
> objects *without* a protecting branch. And these tests already have a
> comment explaining the issue.
>
We don't use weakrefs in the code very much, and where a None can be an
easily accidental return from something where we expect to get an object.
So there isn't a lot of prior art, but I *have* seen a fair number of
AttributeError bugs filed that are hard to discover.
I think using weakref without being careful about its side-effects is
poor form. Even if it "can't happen". Bugs happen.
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk3
oVQAnRXVszFF95i
=1tkW
-----END PGP SIGNATURE-----
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
...
> Could you point me where in the *actual* code this idiom is used and
> such a reporting is taken into account ?
>
> I find it highly unproductive to be blocked from landing incremental
> patches when my intent is to ease review.
I should also mention that I'm not blocking. Specifically I said:
> John
> =:->
>
> review: approve
>
And then later I said:
> So please consider giving us a nicer error for discoverability purposes.
Both are meant as, "I really think this would be better if it trapped
for None, but if you feel strongly you can land it".
Bugs happen, in places we don't want them to. Getting AttributeError is
a really confusing way of finding out that a branch disappeared when we
expected it to exist. If you used a normal reference the code would be
clearer and would *actually* never fail. I believe you are doing this
because of working recently with Martin gz and running into issues with
python cycles taking a while to be closed by the garbage collector.
ATM, I feel that is premature optimization (Branch has about 5 different
cycles, so unless we close them all, it isn't really worth making this
code harder to follow on the off chance that we clean other stuff up.)
I'm willing to live with it, provided we don't make things harder later
because of hard-to-
AttributeError: None has no attribute unlock().
It is obvious to you *now*, will it be obvious in 2 years when we get a
bug report because we cleaned up something that used to have a cycle and
as a side effect kept the branch alive and people start expecting that
behavior.
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk3
dg0An0KtgA3EcUl
=FzKW
-----END PGP SIGNATURE-----
Vincent Ladeuil (vila) wrote : | # |
>>>>> John Arbash Meinel writes:
<snip/>
> We don't use weakrefs in the code very much,
> and where a None can be an easily accidental return from something
> where we expect to get an object.
Not, it's not easy, it has to be deliberate. The branch object used here
is a *mandatory* parameter of the constructor.
Creating such an object and keeping it while *deleting* the branch is
not only outside of the obvious use case, it's deliberate sabotage and
as I already mentioned, you already get a traceback.
> So there isn't a lot of prior art, but I *have* seen a fair number
> of AttributeError bugs filed that are hard to discover.
> I think using weakref without being careful about its side-effects is
> poor form.
Which is why I *am* careful. There is *one* usage so far in a test with
a comment. There will be a *single* other one once this is deployed.
> Even if it "can't happen". Bugs happen.
You still not answer how you want it to be reported. At the point where
it can occur you only have a traceback at best which is already what
will be reported.
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
...
> > I think using weakref without being careful about its side-effects is
> > poor form.
>
> Which is why I *am* careful. There is *one* usage so far in a test with
> a comment. There will be a *single* other one once this is deployed.
>
> > Even if it "can't happen". Bugs happen.
>
> You still not answer how you want it to be reported. At the point where
> it can occur you only have a traceback at best which is already what
> will be reported.
>
branch = self.branch_ref()
if branch is None:
raise RuntimeError('Our branch disappeared, but the branch should'
' be holding a direct reference to this config'
' object. %(s)' % (self,)
Something like that would be much easier to understand if it happens
than just AttributeError(
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk3
ueoAnjyCvEqwqZH
=d4Jf
-----END PGP SIGNATURE-----
Vincent Ladeuil (vila) wrote : | # |
>>>>> John Arbash Meinel <email address hidden> writes:
<snip/>
> And then later I said:
>> So please consider giving us a nicer error for discoverability purposes.
> ^^^^^^^^
Ok, sorry for the knee-jerk reaction.
> Both are meant as, "I really think this would be better if it trapped
> for None,
But trapped to report what ?
We already get a traceback in this case and we can't get more than that.
> but if you feel strongly you can land it".
But I can't land since two pre-requisite are still waiting (and two
already approved branches in addition to this one still can't land
either).
> Bugs happen, in places we don't want them to. Getting
> AttributeError is a really confusing way of finding out that a
> branch disappeared when we expected it to exist.
When this code is deployed the config attribute won't be able to exist
without its containing branch, it's not as if it was intended to live in
weird places.
> If you used a normal reference the code would be clearer
And the cycle well hidden and *that* would be far more harder to track
down.
If you were to encounter a traceback mentioning one of the weafref lines,
the annotations will give you the context where it has been introduced
which itself will show you the tests explaining the rationale.
And this in a class which mentions:
# FIXME: Moreover, we shouldn't need classes for these stores either, factory
# functions or a registry will make it easier and clearer for tests, focusing
# on the relevant parts of the API that needs testing -- vila 20110503 (based
# on a poolie's remark)
So we're still talking about scaffolding stuff here...
> and would *actually* never fail.
Oh right, there will be no bugs to dereference the ref, but the bugs
implied by the cycle are the kind I'd really prefer to avoid.
> I believe you are doing this because of working recently with
> Martin gz and running into issues with python cycles taking a
> while to be closed by the garbage collector.
I'm always paying attention to cycles, some are more obvious than
others. Using closures for example, as gz was trying to avoid, is one
important use case I want to keep and that I discussed with him at
length and where, in tests, we could easily address by using addCleanups
*without* stopping to use closures (see my comments on his first
proposal months ago). Yet, closures doesn't create cycles that the gc
can't deal with which is why I had troubles understanding gz approach
(which requires detecting cycles on objects that are about to be
collectible but not fully at the point he want to check).
Here, the cycle *is* obvious. SmartClientHTTP
case and the pattern is exactly the same: an object keeps a reference to
a parent object which, by design, will live longer than itself.
I was explicitly violating this in the *test* when I encounter the issue
which I why I put the comment there (3 times no less).
Adding some reporting into SmartClientHTTP
adding it there.
> ATM, I feel that is premature optimization (Branch has about 5
> different cycles, so unless we close them all, it i...
Vincent Ladeuil (vila) wrote : | # |
>>>>> John Arbash Meinel <email address hidden> writes:
<snip/>
> branch = self.branch_ref()
> if branch is None:
> raise RuntimeError('Our branch disappeared, but the branch should'
> ' be holding a direct reference to this config'
> ' object. %(s)' % (self,)
> Something like that would be much easier to understand if it happens
> than just AttributeError(
Done, fixing typos, it won't make a big difference, you still need the
backtrace anyway and when it occurs (unlikely but whatever) the bug will
most probably be in code not mentioned there.
Martin Packman (gz) wrote : | # |
From reading the discussion here, it seems there's not really any disagreement on the core points, just a few implementation specifics. The change to raise an assertion looks good to.
Of interest to this topic is bug 758652 where John found there were lots of Branch objects being kept alive leading to loggerhead memory problems. There are two approaches to fixing that, one is to avoid cyclic data structures, such as by using weakref here. The other is a finalize method that neuters the branch, which would include things like manually deleting attributes that formed cycles. While a finalize method doesn't exist, avoiding adding more objects that reference each other is sensible.
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 05/26/2011 06:31 PM, Martin [gz] wrote:
>>From reading the discussion here, it seems there's not really any disagreement on the core points, just a few implementation specifics. The change to raise an assertion looks good to.
>
> Of interest to this topic is bug 758652 where John found there were lots of Branch objects being kept alive leading to loggerhead memory problems. There are two approaches to fixing that, one is to avoid cyclic data structures, such as by using weakref here. The other is a finalize method that neuters the branch, which would include things like manually deleting attributes that formed cycles. While a finalize method doesn't exist, avoiding adding more objects that reference each other is sensible.
I think finalize is more sensible. Branch specifically is just hard to
prevent from cycles, because even stuff like dynamic dispatch causes a
ref-cycle.
For example:
class SimpleSelfRef(
def __init__(self):
self.actions = {'foo': self.do_foo, 'bar': self.do_bar}
def do_foo(self):
return 'foo'
def do_bar(self):
return 'bar'
def do_action(self, action):
return self.actions[
The 'self.actions' dict forms a cycle. I'm not sure where we have that,
but I'm sure we did at one point. And I think Branch hands off
references of functions to Repository, but it has been a long time since
I've looked at it.
It is very natural to write something like:
self.helper = Helper(
And all of that stuff creates cycles. And as long as helper does need
some state from 'self', it is going to have a cycle. The best you could
do is pass self explicitly as a weakref and take_action as a non-bound
method. But it really makes the code harder rather than just .finalize().
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk3
qpgAoMN8dNS0z71
=gVKi
-----END PGP SIGNATURE-----
Andrew Bennetts (spiv) wrote : | # |
Vincent Ladeuil wrote:
[…]
> But trapped to report what ?
>
> We already get a traceback in this case and we can't get more than that.
Tracebacks alone do tell you which line of code broke, but:
1) not all bug reports have tracebacks readily available. e.g. the
smart server doesn't send tracebacks to the client, but does send
the str of the exception.
2) not all bug reports have the exact bzr version included, which makes
matching up line numbers hard. Especially from installs without .py
files, so tracebacks only include file names, line numbers and
function names.
3) Code evolves, and what produces an unambiguous traceback today might
not in bzr 3.1.
Perhaps none of those is going to be an issue here, but I'm unconvinced.
I think John's suggestion to report “weakref of branch disappeared too
early” is clearly more informative than a generic AttributeError, and
not hard to do. If I were very confident we'd never see this error I
wouldn't care, but this (using weakrefs to avoid cycles) is a new idiom
for us and I think it's reasonable to expect we'll make a few mistakes
with it.
Martin Pool (mbp) wrote : | # |
Like John said recently, the bar for review is that things be better
than they are, not that they be perfect. But generally, making sure
that forseeable errors give a sensible message seems to have a very
good payoff.
Martin Pool (mbp) wrote : | # |
The .unload docstring doesn't really make it clear to me who would be supposed to call it, or when. Perhaps it should be in the developer guide for configuration?
36 + # We don't want to create a cycle here when the BranchStore becomes
37 + # part of an object (roughly a Stack, directly or indirectly) that is
38 + # an attribute of the branch object itself. Since the BranchStore
39 + # cannot exist without a branch, it's safe to make it a weakref.
40 + self.branch_ref = weakref.ref(branch)
Why do cycles matter here?
145 + raise AssertionError(
146 + '``server_class`` or none of them')
To be pedantic, you say "neither" if there are two, and "none" if there are more.
Vincent Ladeuil (vila) wrote : | # |
> The .unload docstring doesn't really make it clear to me who would be supposed
> to call it, or when.
I introduced unload as the abstract way to express self._config_obj = None to trigger a reload from the store when needed.
This is required to match the actual behaviour which requires re-reading the whole config file before modifying a single option value.
It will probably be used in the future implementation but only once when writing the Store.
> Perhaps it should be in the developer guide for configuration?
Until I use unload() in the future implementation I'm unsure about whether it will still be needed :-/
>
> 36 + # We don't want to create a cycle here when the BranchStore
> becomes
> 37 + # part of an object (roughly a Stack, directly or indirectly)
> that is
> 38 + # an attribute of the branch object itself. Since the
> BranchStore
> 39 + # cannot exist without a branch, it's safe to make it a
> weakref.
> 40 + self.branch_ref = weakref.ref(branch)
>
> Why do cycles matter here?
Err, I should be missing the point of the question. Assuming you've read the comments above, you know we already have cycles involving branches so I'd rather not introduce a new one when I can avoid it.
>
> 145 + raise AssertionError(
> 146 + '``server_class`` or none of them')
>
> To be pedantic, you say "neither" if there are two, and "none" if there are
> more.
Wow, thanks. Fixed (in two places).
Vincent Ladeuil (vila) wrote : | # |
sent to pqm by email
Martin Pool (mbp) wrote : | # |
>> Assuming you've read the comments above, you know we already have cycles involving branches so I'd rather not introduce a new one when I can avoid it.
I've just re-read the preceding comments about cycles.
My first impression here, apparently the same as John, was that this seemed like premature optimization.
It sounds like you have a rule in your head along the lines of "objects that hold references to long-lived parents should always do so through a weakref", or something like that. As spiv said, "this (using weakrefs to avoid cycles) is a new idiom for us and I think it's reasonable to expect we'll make a few mistakes with it."
Could you perhaps post to the list explaining the general rule you think we ought to follow about avoiding cycles? That may save discussing whether it's necessary case-by-case and it will also help other people use it. Also we can discuss whether we prefer that or a .close or .finalize method.
It seems reasonable that the branch config stay in memory as long as the branch is in memory. Is the problem here (per bug 758652) that we suspect Python is just too bad at collecting cyclic structures and we should just always avoid them?
Martin Pool (mbp) wrote : | # |
OK, I kicked off a thread "weakrefs and avoiding gc cycles"; please reply there.
Preview Diff
1 | === modified file 'bzrlib/config.py' |
2 | --- bzrlib/config.py 2011-05-31 17:31:41 +0000 |
3 | +++ bzrlib/config.py 2011-05-31 17:31:41 +0000 |
4 | @@ -2167,6 +2167,15 @@ |
5 | """ |
6 | raise NotImplementedError(self._load_from_string) |
7 | |
8 | + def unload(self): |
9 | + """Unloads the Store. |
10 | + |
11 | + This should make is_loaded() return False. This is used when the caller |
12 | + knows that the persistent storage has changed or may have change since |
13 | + the last load. |
14 | + """ |
15 | + raise NotImplementedError(self.unload) |
16 | + |
17 | def save(self): |
18 | """Saves the Store to persistent storage.""" |
19 | raise NotImplementedError(self.save) |
20 | @@ -2220,6 +2229,9 @@ |
21 | def is_loaded(self): |
22 | return self._config_obj != None |
23 | |
24 | + def unload(self): |
25 | + self._config_obj = None |
26 | + |
27 | def load(self): |
28 | """Load the store from the associated file.""" |
29 | if self.is_loaded(): |
30 | @@ -2370,14 +2382,25 @@ |
31 | def __init__(self, branch): |
32 | super(BranchStore, self).__init__(branch.control_transport, |
33 | 'branch.conf') |
34 | - # FIXME: This creates a cycle -- vila 2011-05-27 |
35 | - self.branch = branch |
36 | + # We don't want to create a cycle here when the BranchStore becomes |
37 | + # part of an object (roughly a Stack, directly or indirectly) that is |
38 | + # an attribute of the branch object itself. Since the BranchStore |
39 | + # cannot exist without a branch, it's safe to make it a weakref. |
40 | + self.branch_ref = weakref.ref(branch) |
41 | + |
42 | + def _get_branch(self): |
43 | + b = self.branch_ref() |
44 | + if b is None: |
45 | + # Programmer error, a branch store can't exist if the branch it |
46 | + # refers to is dead. |
47 | + raise AssertionError('Dead branch ref in %r' % (self,)) |
48 | + return b |
49 | |
50 | def lock_write(self, token=None): |
51 | - return self.branch.lock_write(token) |
52 | + return self._get_branch().lock_write(token) |
53 | |
54 | def unlock(self): |
55 | - return self.branch.unlock() |
56 | + return self._get_branch().unlock() |
57 | |
58 | @needs_write_lock |
59 | def save(self): |
60 | @@ -2578,8 +2601,8 @@ |
61 | """ |
62 | |
63 | def set(self, name, value): |
64 | - # Force a reload (assuming we use a LockableIniFileStore) |
65 | - self.store._config_obj = None |
66 | + # Force a reload |
67 | + self.store.unload() |
68 | super(_CompatibleStack, self).set(name, value) |
69 | # Force a write to persistent storage |
70 | self.store.save() |
71 | @@ -2602,7 +2625,6 @@ |
72 | super(LocationStack, self).__init__( |
73 | [matcher.get_sections, gstore.get_sections], lstore) |
74 | |
75 | -# FIXME: See BranchStore, same remarks -- vila 20110512 |
76 | class BranchStack(_CompatibleStack): |
77 | |
78 | def __init__(self, branch): |
79 | @@ -2613,6 +2635,7 @@ |
80 | super(BranchStack, self).__init__( |
81 | [matcher.get_sections, bstore.get_sections, gstore.get_sections], |
82 | bstore) |
83 | + self.branch = branch |
84 | |
85 | |
86 | class cmd_config(commands.Command): |
87 | @@ -2794,7 +2817,7 @@ |
88 | # object. |
89 | test_store_builder_registry = registry.Registry() |
90 | |
91 | -# Thre registered object should be a callable receiving a test instance |
92 | +# The registered object should be a callable receiving a test instance |
93 | # parameter (inheriting from tests.TestCaseWithTransport) and returning a Stack |
94 | # object. |
95 | test_stack_builder_registry = registry.Registry() |
96 | |
97 | === modified file 'bzrlib/tests/test_config.py' |
98 | --- bzrlib/tests/test_config.py 2011-05-31 17:31:41 +0000 |
99 | +++ bzrlib/tests/test_config.py 2011-05-31 17:31:41 +0000 |
100 | @@ -41,6 +41,7 @@ |
101 | trace, |
102 | transport, |
103 | ) |
104 | +from bzrlib.transport import remote |
105 | from bzrlib.tests import ( |
106 | features, |
107 | TestSkipped, |
108 | @@ -72,33 +73,99 @@ |
109 | config.test_store_builder_registry.register( |
110 | 'location', lambda test: config.LocationStore()) |
111 | |
112 | + |
113 | +def build_backing_branch(test, relpath, |
114 | + transport_class=None, server_class=None): |
115 | + """Test helper to create a backing branch only once. |
116 | + |
117 | + Some tests needs multiple stores/stacks to check concurrent update |
118 | + behaviours. As such, they need to build different branch *objects* even if |
119 | + they share the branch on disk. |
120 | + |
121 | + :param relpath: The relative path to the branch. (Note that the helper |
122 | + should always specify the same relpath). |
123 | + |
124 | + :param transport_class: The Transport class the test needs to use. |
125 | + |
126 | + :param server_class: The server associated with the ``transport_class`` |
127 | + above. |
128 | + |
129 | + Either both or neither of ``transport_class`` and ``server_class`` should |
130 | + be specified. |
131 | + """ |
132 | + if transport_class is not None and server_class is not None: |
133 | + test.transport_class = transport_class |
134 | + test.transport_server = server_class |
135 | + elif not (transport_class is None and server_class is None): |
136 | + raise AssertionError('Specify both ``transport_class`` and ' |
137 | + '``server_class`` or neither of them') |
138 | + if getattr(test, 'backing_branch', None) is None: |
139 | + # First call, let's build the branch on disk |
140 | + test.backing_branch = test.make_branch(relpath) |
141 | + |
142 | + |
143 | +def keep_branch_alive(test, b): |
144 | + """Keep a branch alive for the duration of a test. |
145 | + |
146 | + :param tests: the test that should hold the branch alive. |
147 | + |
148 | + :param b: the branch that should be kept alive. |
149 | + |
150 | + Several tests need to keep a reference to a branch object as they are |
151 | + testing a Store which uses a weak reference. This is achieved by embedding |
152 | + a reference to the branch object in a lambda passed to a cleanup. When the |
153 | + test finish the cleanup method is deleted and so does the reference to the |
154 | + branch. |
155 | + """ |
156 | + test.addCleanup(lambda : b) |
157 | + |
158 | + |
159 | def build_branch_store(test): |
160 | - if getattr(test, 'branch', None) is None: |
161 | - test.branch = test.make_branch('branch') |
162 | - # Since we can be called to create different stores, we need to build them |
163 | - # from different branch *objects*, even if they point to the same branch on |
164 | - # disk, otherwise tests about conccurent updates won't be able to trigger |
165 | - # LockContention |
166 | - return config.BranchStore(branch.Branch.open('branch')) |
167 | + build_backing_branch(test, 'branch') |
168 | + b = branch.Branch.open('branch') |
169 | + keep_branch_alive(test, b) |
170 | + return config.BranchStore(b) |
171 | config.test_store_builder_registry.register('branch', build_branch_store) |
172 | |
173 | |
174 | +def build_remote_branch_store(test): |
175 | + # There is only one permutation (but we won't be able to handle more with |
176 | + # this design anyway) |
177 | + (transport_class, server_class) = remote.get_test_permutations()[0] |
178 | + build_backing_branch(test, 'branch', transport_class, server_class) |
179 | + b = branch.Branch.open(test.get_url('branch')) |
180 | + keep_branch_alive(test, b) |
181 | + return config.BranchStore(b) |
182 | +config.test_store_builder_registry.register('remote_branch', |
183 | + build_remote_branch_store) |
184 | + |
185 | + |
186 | config.test_stack_builder_registry.register( |
187 | 'bazaar', lambda test: config.GlobalStack()) |
188 | config.test_stack_builder_registry.register( |
189 | 'location', lambda test: config.LocationStack('.')) |
190 | |
191 | + |
192 | def build_branch_stack(test): |
193 | - if getattr(test, 'branch', None) is None: |
194 | - test.branch = test.make_branch('branch') |
195 | - # Since we can be called to create different stacks, we need to build them |
196 | - # from different branch *objects*, even if they point to the same branch on |
197 | - # disk, otherwise tests about conccurent updates won't be able to trigger |
198 | - # LockContention |
199 | - return config.BranchStack(branch.Branch.open('branch')) |
200 | + build_backing_branch(test, 'branch') |
201 | + b = branch.Branch.open('branch') |
202 | + keep_branch_alive(test, b) |
203 | + return config.BranchStack(b) |
204 | config.test_stack_builder_registry.register('branch', build_branch_stack) |
205 | |
206 | |
207 | +def build_remote_branch_stack(test): |
208 | + # There is only one permutation (but we won't be able to handle more with |
209 | + # this design anyway) |
210 | + (transport_class, server_class) = remote.get_test_permutations()[0] |
211 | + build_backing_branch(test, 'branch', transport_class, server_class) |
212 | + b = branch.Branch.open(test.get_url('branch')) |
213 | + keep_branch_alive(test, b) |
214 | + return config.BranchStack(b) |
215 | +config.test_stack_builder_registry.register('remote_branch', |
216 | + build_remote_branch_stack) |
217 | + |
218 | + |
219 | sample_long_alias="log -r-15..-1 --line" |
220 | sample_config_text = u""" |
221 | [DEFAULT] |
222 | @@ -642,11 +709,11 @@ |
223 | def test_default_is_True(self): |
224 | self.config = self.get_config(True) |
225 | self.assertExpandIs(True) |
226 | - |
227 | + |
228 | def test_default_is_False(self): |
229 | self.config = self.get_config(False) |
230 | self.assertExpandIs(False) |
231 | - |
232 | + |
233 | |
234 | class TestIniConfigOptionExpansion(tests.TestCase): |
235 | """Test option expansion from the IniConfig level. |
236 | @@ -1940,7 +2007,6 @@ |
237 | |
238 | def setUp(self): |
239 | super(TestReadonlyStore, self).setUp() |
240 | - self.branch = self.make_branch('branch') |
241 | |
242 | def test_building_delays_load(self): |
243 | store = self.get_store(self) |
244 | @@ -1988,7 +2054,9 @@ |
245 | return self.transport.has(store_basename) |
246 | |
247 | def test_save_empty_creates_no_file(self): |
248 | - if self.store_id == 'branch': |
249 | + # FIXME: There should be a better way than relying on the test |
250 | + # parametrization to identify branch.conf -- vila 2011-0526 |
251 | + if self.store_id in ('branch', 'remote_branch'): |
252 | raise tests.TestNotApplicable( |
253 | 'branch.conf is *always* created when a branch is initialized') |
254 | store = self.get_store(self) |
255 | @@ -2007,7 +2075,9 @@ |
256 | self.assertLength(0, sections) |
257 | |
258 | def test_save_with_content_succeeds(self): |
259 | - if self.store_id == 'branch': |
260 | + # FIXME: There should be a better way than relying on the test |
261 | + # parametrization to identify branch.conf -- vila 2011-0526 |
262 | + if self.store_id in ('branch', 'remote_branch'): |
263 | raise tests.TestNotApplicable( |
264 | 'branch.conf is *always* created when a branch is initialized') |
265 | store = self.get_store(self) |
266 | @@ -2114,7 +2184,28 @@ |
267 | self.assertPathExists('dir/subdir') |
268 | |
269 | |
270 | +class TestBranchStore(TestStore): |
271 | + |
272 | + def test_dead_branch(self): |
273 | + build_backing_branch(self, 'branch') |
274 | + b = branch.Branch.open('branch') |
275 | + store = config.BranchStore(b) |
276 | + del b |
277 | + # The only reliable way to trigger the error is to explicitly call the |
278 | + # garbage collector. |
279 | + import gc |
280 | + gc.collect() |
281 | + store.get_mutable_section(None).set('foo', 'bar') |
282 | + self.assertRaises(AssertionError, store.save) |
283 | + |
284 | + |
285 | class TestConcurrentStoreUpdates(TestStore): |
286 | + """Test that Stores properly handle conccurent updates. |
287 | + |
288 | + New Store implementation may fail some of these tests but until such |
289 | + implementations exist it's hard to properly filter them from the scenarios |
290 | + applied here. If you encounter such a case, contact the bzr devs. |
291 | + """ |
292 | |
293 | scenarios = [(key, {'get_stack': builder}) for key, builder |
294 | in config.test_stack_builder_registry.iteritems()] |
On 26 May 2011 16:33, Vincent Ladeuil <email address hidden> wrote: lock_write( token) ref().lock_ write(token) unlock( ) ref().unlock( )
> @@ -2372,13 +2384,13 @@
> # part of an object (roughly a Stack, directly or indirectly) that is
> # an attribute of the branch object itself. Since the BranchStore
> # cannot exist without a branch, it's safe to make it a weakref.
> - self.branch = weakref.ref(branch)
> + self.branch_ref = weakref.ref(branch)
>
> def lock_write(self, token=None):
> - return self.branch.
> + return self.branch_
>
> def unlock(self):
> - return self.branch.
> + return self.branch_
>
> @needs_write_lock
> def save(self):
How does that work?