Merge lp:~vila/bzr/832042-shared-stores into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: 6554
Proposed branch: lp:~vila/bzr/832042-shared-stores
Merge into: lp:bzr
Diff against target: 489 lines (+141/-56)
13 files modified
bzrlib/config.py (+62/-9)
bzrlib/library_state.py (+6/-0)
bzrlib/lockdir.py (+1/-1)
bzrlib/plugins/launchpad/test_register.py (+4/-0)
bzrlib/remote.py (+2/-2)
bzrlib/tests/__init__.py (+35/-29)
bzrlib/tests/blackbox/test_init.py (+4/-2)
bzrlib/tests/blackbox/test_serve.py (+2/-3)
bzrlib/tests/test_config.py (+9/-0)
bzrlib/tests/test_debug.py (+7/-4)
bzrlib/tests/test_lockdir.py (+1/-1)
bzrlib/tests/test_ui.py (+1/-2)
doc/en/release-notes/bzr-2.6.txt (+7/-3)
To merge this branch: bzr merge lp:~vila/bzr/832042-shared-stores
Reviewer Review Type Date Requested Status
Martin Packman (community) Approve
Review via email: mp+118110@code.launchpad.net

Commit message

Share and cache local config files

Description of the change

This fixes bug #832042 by sharing and caching local config files, opening
the doors wide for adding more config options or files.

The implementation is the simplest one: the library state gains a dict
attribute for all used stores.

A fallback is needed for scripts that never call bzrlib.intialize() until we
fix the bug(s) about always requiring a library state object to be available
whatever script is used and pass these state objects around in the code base
(repo/ranch/wts should know about which state they are used with (somehow
like transports need to be shared)).

The fallback is a bit ugly, but not much than missing the library state :)

Compared to the corresponding change for branches config file, this one if
far lighter and only a couple of tests needed to be fixed. Even there the
use case is at the limit to what is expected to be supported: communication
with a subprocess (for which config files are a bad medium anyway).

To describe impact, numbers speak better than words, so, with the following
patch:

=== modified file 'bzrlib/lockdir.py'
--- bzrlib/lockdir.py 2011-12-19 13:23:58 +0000
+++ bzrlib/lockdir.py 2012-08-02 08:52:49 +0000
@@ -858,6 +858,6 @@
     as it gives some clue who the user is.
     """
     try:
- return config.GlobalConfig().username()
+ return config.GlobalStack().get('email')
     except errors.NoWhoami:
         return osutils.getuser_unicode()

Running the full test suite with -Econfig_stats allows going from:

config.load : 53265
config.save : 4005
config.get : 453640
config.set : 4667
config.remove : 26
old_config.load : 294868
old_config.save : 608
old_config.get : 166067
old_config.set : 475
old_config.remove: 6

to

config.load : 53342
config.save : 4005
config.get : 627394
config.set : 4667
config.remove : 26
old_config.load : 124371
old_config.save : 608
old_config.get : 166067
old_config.set : 475
old_config.remove: 6

I.e. old_config.load goes from 294,868 to 124,371 (-170,497) whereas
config.load goes from 53,265 to 53,342 (+77), or in marketing speak a
221,424% improvement !11!! ;)

More seriously, in this specific case, most of the tests rely on BZR_EMAIL
anyway so what the new design really provides is *avoiding* the IO since the
env var takes precedence (whereas the old design would read the file anyway
*before* looking at the env var). This explains why config.get goes from
453640 to 627394 (+ 173,754) whereas old_config.get stays stable and
config.load only goes up moderately.

So overall, the first revno I started collected statistics for is 5981:

config.load : 296
config.save : 106
config.get : 82
config.set : 52
config.remove : 8
old_config.load : 1002308
old_config.save : 5018
old_config.get : 383768
old_config.set : 4873
old_config.remove: 14

the significant ratio being old_config.get / old_config.load: 0.38

and with this proposal (with still some old config uses) we are at:

config.load : 53310
config.save : 4006
config.get : 627375
config.set : 4686
config.remove : 26
old_config.load : 124352
old_config.save : 588
old_config.get : 166068
old_config.set : 456
old_config.remove: 6

config.get / config.load ratio: 11.76

Since there were a bunch of changes the numbers cannot be compared directly
bu the trend is obvious a single IO for multiple config options instead of
several IOs for a single option. In proper English: feel free to use more
options and config files, they are basically free once you use at least one
(which all bzr commands do).

This patch also provides fresh shared stores for tests or they'll use the
fallback which means they aren't saved until selftest exits (which make no
test (well almost) fail but is still a kind of leak).

On a related note, many tests use TestCaseWithMemoryTransport and/or
branchbuilder relying on the limited ability to handle a tree/branch/repo in
a MemoryTransport, including locking. Locking use the 'email' config option
and as such looks for bazaar.conf... which doesn't exist for a
MemoryTransport... which is fine since we have a sane default and bzr copes
with non-existing config files. Pfew. I wrongly chased these tests as
potential culprits for not using TestCaseInTempDir before realizing they
were indeed allowed to run without an existing bazaar.conf. I thought it was
worth mentioning for those following from home. This is why _shared_stores
is overriden during setup at the TestCaseWithMemoryTransport level instead
of TestCaseInTempDir.

I moved setUp at the beginning of the class for TestCaseWithMemoryTransport
and TestCaseWithTransport as I was fed up with missing them otherwise
(that's the usual pattern in the rest of the code base anyway).

Also:

- split the eager test in bt.test_debug,
- use proper unique urls for remote config files (for both better error
  messages and sharing),

Next possible steps are to:
- remove (finally !) BranchOnlyStack since the cheap optimization that
  triggered its need is not worth the code duplication and complexity
  anymore,
- properly test the library state stuff (http://pad.lv/865368) and get rid
  of the fallback.

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :

Can I have a review ? Pretty please ?

Revision history for this message
Martin Packman (gz) wrote :

Nice improvement, and all the test updates look good to me. A few comments, only the last really needs some kind of action.

+_shared_stores = {}
+_shared_stores_at_exit_installed = False

Rather than using two variables for this hack, could just start with `_shared_stores = None` then set to empty dict and add atexit function if None.

+ url = store.external_url()
+ try:
+ return stores[url]
+ except KeyError:
+ stores[url] = store

This introduces another incidence of assuming that transport.base is normalised, and also requires a one-to-one mapping between urls and underlying configs across the library lifetime. As only GlobalStore and LocationStore get passed here and we don't expect the return of config_dir() to change across the lifetime of the process (outside of tests which you handle), this is okay. Some documentation pointing out that shared stores are only for per-process-user not per-branch config would be good.

The shared stores dict is also an unbounded cache. It would be possible to use weakrefs, which would also remove the need for seperate process exit cleanup. Given we only expect 2(?) items in the dict the current code is fine.

Always initialising a GlobalStore/LocalStore object to pass in to get_shared_store, then possibly getting a different one back is also inelegant. Why not expect a class rather than instance passed as store to the function instead, which would then enforce the Store could be constructed with no arguments. It seems the possible_transports arg is never given anyway?

+ if exc_type is None:
+ # Save config changes
+ for k, store in self.config_stores.iteritems():
+ store.save_changes()

Why only save config changes if no error has been raised? This logic differs from the pre-library state version where the atexit function will run regardless. Always running but catching errors seems like a better behaviour, the error may be due to some completely unrelated later action and losing an earlier config change would be suprising.

review: Approve
Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (4.4 KiB)

> Nice improvement, and all the test updates look good to me. A few comments,
> only the last really needs some kind of action.
>
> +_shared_stores = {}
> +_shared_stores_at_exit_installed = False
>
> Rather than using two variables for this hack, could just start with
> `_shared_stores = None` then set to empty dict and add atexit function if
> None.

But then the same trick should be applied in library_state and make the hack even uglier, I'd rather stop uglifying here ;)

>
> + url = store.external_url()
> + try:
> + return stores[url]
> + except KeyError:
> + stores[url] = store
>
> This introduces another incidence of assuming that transport.base is
> normalised, and also requires a one-to-one mapping between urls and underlying
> configs across the library lifetime.

Well, U in url stands for unique and the design here is that you cannot have different stores at the same url.

I know we had (and still have) issues with badly normalized urls but here there aren't several ways to get a store.

> As only GlobalStore and LocationStore get
> passed here and we don't expect the return of config_dir() to change across
> the lifetime of the process (outside of tests which you handle), this is okay.
> Some documentation pointing out that shared stores are only for per-process-
> user not per-branch config would be good.

Yup, comments added.

>
> The shared stores dict is also an unbounded cache.

Right, that why it should be part of the library state and taken care of there.

> It would be possible to use
> weakrefs, which would also remove the need for seperate process exit cleanup.

I fail to see why weakrefs will help there...

> Given we only expect 2(?) items in the dict the current code is fine.
>
> Always initialising a GlobalStore/LocalStore object to pass in to
> get_shared_store, then possibly getting a different one back is also
> inelegant.

Well, you get a different one "pysically", but since stores are uniquely identified by urls, this *has* to be the same "logical" one.

> Why not expect a class rather than instance passed as store to the
> function instead,

See below.

> which would then enforce the Store could be constructed with
> no arguments.

And why should we force this decision *inside* some inaccessible method/function ? Letting the caller build its own object however it sees fit reduce coupling, that's the point.

> It seems the possible_transports arg is never given anyway?

Not until bzrlib implement all working tree operations across the whole code base, no.

The actual design for Stores is that they use only transport operations, it has been discussed in the past that local config files doesn't *have* to but there is no gain in using direct system calls so I stick with the transport API.

>
> + if exc_type is None:
> + # Save config changes
> + for k, store in self.config_stores.iteritems():
> + store.save_changes()
>
> Why only save config changes if no error has been raised?

Because across bzrlib, config changes are supposed to be saved only when the operation succeeds. If there is a special need to save these (or some...

Read more...

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

sent to pqm by email

Revision history for this message
Martin Packman (gz) wrote :

> > Rather than using two variables for this hack, could just start with
> > `_shared_stores = None` then set to empty dict and add atexit function if
> > None.
>
> But then the same trick should be applied in library_state and make the hack
> even uglier, I'd rather stop uglifying here ;)

The point is library_state only has a single variable and handles cleanup regardless.

> > This introduces another incidence of assuming that transport.base is
> > normalised, and also requires a one-to-one mapping between urls and
> underlying
> > configs across the library lifetime.
>
> Well, U in url stands for unique and the design here is that you cannot have
> different stores at the same url.

That's a gotcha, it actually stands for 'uniform resource locator' and there's no guarentee of uniqueness. Webservers are perfectly within their rights to 'internally redirect' different urls to the same location, and expose multiple resources under the same url.

As mentioned, only sharing local configuration is fine though.

> I fail to see why weakrefs will help there...

You can pass a cleanup function that runs when a weakref is collected, which could save as soon as the config reference was dropped. Would also change behaviour back to re-opening config files unless some part of bzrlib was seperately persisting them, so isn't really what we want for the global/location case.

> And why should we force this decision *inside* some inaccessible
> method/function ? Letting the caller build its own object however it sees fit
> reduce coupling, that's the point.

It does extra work, that all but the first config fetch will be immediately discarded. As this touches disk and so on, it's not entirely trivial, constructing GlobalStore times at 212 usec here. Not very important though, and passing possible_transports would probably also do.

> Not until bzrlib implement all working tree operations across the whole code
> base, no.

I don't understand this...

> I disagree there and we had (and probably still have) cases where some config
> changes are saved *despite* an error occurring (to be honest I'm thinking more
> about branch options here).

I'm less worried about the specifics of the behaviour, and more about it diverging depending on if bzrlib.initialize is used. Perhaps that's not easily avoidable, but if the stores in question are seldom modified by bzr (outside of `bzr config`?) as you suggest then we're unlikely to run into problems. Not relying on library state cleanup would be much nicer, but you know the tradeoffs better than me here.

Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (4.5 KiB)

    >> > Rather than using two variables for this hack, could just start with
    >> > `_shared_stores = None` then set to empty dict and add atexit function if
    >> > None.
    >>
    >> But then the same trick should be applied in library_state and make the hack
    >> even uglier, I'd rather stop uglifying here ;)

    > The point is library_state only has a single variable and handles
    > cleanup regardless.

Indeed, and that should be only place when the hack is deleted.

    >> > This introduces another incidence of assuming that transport.base is
    >> > normalised, and also requires a one-to-one mapping between urls and
    >> underlying
    >> > configs across the library lifetime.
    >>
    >> Well, U in url stands for unique and the design here is that you cannot have
    >> different stores at the same url.

    > That's a gotcha, it actually stands for 'uniform resource locator'
    > and there's no guarentee of uniqueness. Webservers are perfectly
    > within their rights to 'internally redirect' different urls to the
    > same location, and expose multiple resources under the same url.

Ha right, uniform not unique, sorry. But the point here is that the same
url cannot provide different stores. Now, if your concern is that people
may be able to access the same store via several urls, well, too bad for
them, they won't share. Will that be an issue ? Not more than trying to
update the same config files from two different processes which will
indicate a problem somewhere (and should lead to warnings if the same
options are modified).

    > As mentioned, only sharing local configuration is fine though.

    >> I fail to see why weakrefs will help there...

    > You can pass a cleanup function that runs when a weakref is
    > collected, which could save as soon as the config reference was
    > dropped.

As you said below, that would save the changes too often (and trigger a
file reload too). The model intended here is to consider the bzrlib
operations atomic rather than the config option modifications. That's
why a library state should be associated to such operations (I'm talking
high-level operations here, roughly the bzr commands).

    > Would also change behaviour back to re-opening config files unless
    > some part of bzrlib was seperately persisting them, so isn't
    > really what we want for the global/location case.

Yup, that's the idea, the file should be opened only once for a given
scope and that scope should be the library state (strictly speaking it
could be the process itself but the lib state is good enough).

    >> And why should we force this decision *inside* some inaccessible
    >> method/function ? Letting the caller build its own object however it sees fit
    >> reduce coupling, that's the point.

    > It does extra work,

Minimal, no IO occurs when building the store, everything is delayed up
to the first option access.

    > that all but the first config fetch will be immediately
    > discarded. As this touches disk and so on, it's not entirely
    > trivial, constructing GlobalStore times at 212 usec here.

Yeah, and how much for a GlobalConfig ?

    > Not very important though, and...

Read more...

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

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/config.py'
2--- bzrlib/config.py 2012-08-01 09:27:40 +0000
3+++ bzrlib/config.py 2012-08-23 14:01:19 +0000
4@@ -3288,6 +3288,7 @@
5 # anyway.
6 return 'In-Process Store, no URL'
7
8+
9 class TransportIniFileStore(IniFileStore):
10 """IniFileStore that loads files from a transport.
11
12@@ -3383,6 +3384,10 @@
13 # on the relevant parts of the API that needs testing -- vila 20110503 (based
14 # on a poolie's remark)
15 class GlobalStore(LockableIniFileStore):
16+ """A config store for global options.
17+
18+ There is a single GlobalStore for a given process.
19+ """
20
21 def __init__(self, possible_transports=None):
22 t = transport.get_transport_from_path(
23@@ -3392,6 +3397,10 @@
24
25
26 class LocationStore(LockableIniFileStore):
27+ """A config store for global options.
28+
29+ There is a single GlobalStore for a given process.
30+ """
31
32 def __init__(self, possible_transports=None):
33 t = transport.get_transport_from_path(
34@@ -3401,6 +3410,10 @@
35
36
37 class BranchStore(TransportIniFileStore):
38+ """A config store for branch options.
39+
40+ There is a single BranchStore for a given branch.
41+ """
42
43 def __init__(self, branch):
44 super(BranchStore, self).__init__(branch.control_transport,
45@@ -3623,6 +3636,10 @@
46 yield is_ref, chunk
47 is_ref = not is_ref
48
49+# FIXME: _shared_stores should be an attribute of a library state once a
50+# library_state object is always available.
51+_shared_stores = {}
52+_shared_stores_at_exit_installed = False
53
54 class Stack(object):
55 """A stack of configurations where an option can be defined"""
56@@ -3817,11 +3834,47 @@
57 return "<config.%s(%s)>" % (self.__class__.__name__, id(self))
58
59 def _get_overrides(self):
60- # Hack around library_state.initialize never called
61+ # FIXME: Hack around library_state.initialize never called
62 if bzrlib.global_state is not None:
63 return bzrlib.global_state.cmdline_overrides.get_sections()
64 return []
65
66+ def get_shared_store(self, store, state=None):
67+ """Get a known shared store.
68+
69+ Store urls uniquely identify them and are used to ensure a single copy
70+ is shared across all users.
71+
72+ :param store: The store known to the caller.
73+
74+ :param state: The library state where the known stores are kept.
75+
76+ :returns: The store received if it's not a known one, an already known
77+ otherwise.
78+ """
79+ if state is None:
80+ state = bzrlib.global_state
81+ if state is None:
82+ global _shared_stores_at_exit_installed
83+ stores = _shared_stores
84+ def save_config_changes():
85+ for k, store in stores.iteritems():
86+ store.save_changes()
87+ if not _shared_stores_at_exit_installed:
88+ # FIXME: Ugly hack waiting for library_state to always be
89+ # available. -- vila 20120731
90+ import atexit
91+ atexit.register(save_config_changes)
92+ _shared_stores_at_exit_installed = True
93+ else:
94+ stores = state.config_stores
95+ url = store.external_url()
96+ try:
97+ return stores[url]
98+ except KeyError:
99+ stores[url] = store
100+ return store
101+
102
103 class MemoryStack(Stack):
104 """A configuration stack defined from a string.
105@@ -3877,7 +3930,7 @@
106 self.store.save()
107
108
109-class GlobalStack(_CompatibleStack):
110+class GlobalStack(Stack):
111 """Global options only stack.
112
113 The following sections are queried:
114@@ -3891,14 +3944,14 @@
115 """
116
117 def __init__(self):
118- gstore = GlobalStore()
119+ gstore = self.get_shared_store(GlobalStore())
120 super(GlobalStack, self).__init__(
121 [self._get_overrides,
122 NameMatcher(gstore, 'DEFAULT').get_sections],
123 gstore, mutable_section_id='DEFAULT')
124
125
126-class LocationStack(_CompatibleStack):
127+class LocationStack(Stack):
128 """Per-location options falling back to global options stack.
129
130
131@@ -3920,10 +3973,10 @@
132 """Make a new stack for a location and global configuration.
133
134 :param location: A URL prefix to """
135- lstore = LocationStore()
136+ lstore = self.get_shared_store(LocationStore())
137 if location.startswith('file://'):
138 location = urlutils.local_path_from_url(location)
139- gstore = GlobalStore()
140+ gstore = self.get_shared_store(GlobalStore())
141 super(LocationStack, self).__init__(
142 [self._get_overrides,
143 LocationMatcher(lstore, location).get_sections,
144@@ -3951,9 +4004,9 @@
145 """
146
147 def __init__(self, branch):
148- lstore = LocationStore()
149+ lstore = self.get_shared_store(LocationStore())
150 bstore = branch._get_config_store()
151- gstore = GlobalStore()
152+ gstore = self.get_shared_store(GlobalStore())
153 super(BranchStack, self).__init__(
154 [self._get_overrides,
155 LocationMatcher(lstore, branch.base).get_sections,
156@@ -3981,7 +4034,7 @@
157 # unlock saves all the changes.
158
159
160-class RemoteControlStack(_CompatibleStack):
161+class RemoteControlStack(Stack):
162 """Remote control-only options stack."""
163
164 # FIXME 2011-11-22 JRV This should probably be renamed to avoid confusion
165
166=== modified file 'bzrlib/library_state.py'
167--- bzrlib/library_state.py 2011-12-19 13:23:58 +0000
168+++ bzrlib/library_state.py 2012-08-23 14:01:19 +0000
169@@ -76,6 +76,8 @@
170 # There is no overrides by default, they are set later when the command
171 # arguments are parsed.
172 self.cmdline_overrides = config.CommandLineStore()
173+ # No config stores are cached to start with
174+ self.config_stores = {} # By url
175 self.started = False
176
177 def __enter__(self):
178@@ -106,6 +108,10 @@
179 self.started = True
180
181 def __exit__(self, exc_type, exc_val, exc_tb):
182+ if exc_type is None:
183+ # Save config changes
184+ for k, store in self.config_stores.iteritems():
185+ store.save_changes()
186 self.cleanups.cleanup_now()
187 trace._flush_stdout_stderr()
188 trace._flush_trace()
189
190=== modified file 'bzrlib/lockdir.py'
191--- bzrlib/lockdir.py 2011-12-19 13:23:58 +0000
192+++ bzrlib/lockdir.py 2012-08-23 14:01:19 +0000
193@@ -858,6 +858,6 @@
194 as it gives some clue who the user is.
195 """
196 try:
197- return config.GlobalConfig().username()
198+ return config.GlobalStack().get('email')
199 except errors.NoWhoami:
200 return osutils.getuser_unicode()
201
202=== modified file 'bzrlib/plugins/launchpad/test_register.py'
203--- bzrlib/plugins/launchpad/test_register.py 2012-02-14 17:22:37 +0000
204+++ bzrlib/plugins/launchpad/test_register.py 2012-08-23 14:01:19 +0000
205@@ -331,6 +331,9 @@
206 service = LaunchpadService()
207 g_conf = config.GlobalStack()
208 g_conf.set('email', 'Test User <test@user.com>')
209+ g_conf.store.save()
210+ # FIXME: auth_path base dir exists only because bazaar.conf has just
211+ # been saved, brittle... -- vila 20120731
212 f = open(auth_path, 'wb')
213 try:
214 scheme, hostinfo = urlparse.urlsplit(service.service_url)[:2]
215@@ -352,6 +355,7 @@
216 self.assertIs(None, service.registrant_password)
217 g_conf = config.GlobalStack()
218 g_conf.set('email', 'Test User <test@user.com>')
219+ g_conf.store.save()
220 stdout = tests.StringIOWrapper()
221 stderr = tests.StringIOWrapper()
222 ui.ui_factory = tests.TestUIFactory(stdin='userpass\n',
223
224=== modified file 'bzrlib/remote.py'
225--- bzrlib/remote.py 2012-07-19 18:28:25 +0000
226+++ bzrlib/remote.py 2012-08-23 14:01:19 +0000
227@@ -384,7 +384,7 @@
228 self._real_store = _mod_config.ControlStore(self.bzrdir)
229
230 def external_url(self):
231- return self.bzrdir.user_url
232+ return urlutils.join(self.branch.user_url, 'control.conf')
233
234 def _load_content(self):
235 medium = self.bzrdir._client._medium
236@@ -3251,7 +3251,7 @@
237 self._real_store = None
238
239 def external_url(self):
240- return self.branch.user_url
241+ return urlutils.join(self.branch.user_url, 'branch.conf')
242
243 def _load_content(self):
244 path = self.branch._remote_path()
245
246=== modified file 'bzrlib/tests/__init__.py'
247--- bzrlib/tests/__init__.py 2012-08-03 09:35:46 +0000
248+++ bzrlib/tests/__init__.py 2012-08-23 14:01:19 +0000
249@@ -1001,6 +1001,8 @@
250 def setUp(self):
251 super(TestCase, self).setUp()
252
253+ # At this point we're still accessing the config files in $BZR_HOME (as
254+ # set by the user running selftest).
255 timeout = config.GlobalStack().get('selftest.timeout')
256 if timeout:
257 timeout_fixture = fixtures.TimeoutFixture(timeout)
258@@ -2447,6 +2449,34 @@
259 self.transport_readonly_server = None
260 self.__vfs_server = None
261
262+ def setUp(self):
263+ super(TestCaseWithMemoryTransport, self).setUp()
264+
265+ def _add_disconnect_cleanup(transport):
266+ """Schedule disconnection of given transport at test cleanup
267+
268+ This needs to happen for all connected transports or leaks occur.
269+
270+ Note reconnections may mean we call disconnect multiple times per
271+ transport which is suboptimal but seems harmless.
272+ """
273+ self.addCleanup(transport.disconnect)
274+
275+ _mod_transport.Transport.hooks.install_named_hook('post_connect',
276+ _add_disconnect_cleanup, None)
277+
278+ self._make_test_root()
279+ self.addCleanup(os.chdir, os.getcwdu())
280+ self.makeAndChdirToTestDir()
281+ self.overrideEnvironmentForTesting()
282+ self.__readonly_server = None
283+ self.__server = None
284+ self.reduceLockdirTimeout()
285+ # Each test may use its own config files even if the local config files
286+ # don't actually exist. They'll rightly fail if they try to create them
287+ # though.
288+ self.overrideAttr(config, '_shared_stores', {})
289+
290 def get_transport(self, relpath=None):
291 """Return a writeable transport.
292
293@@ -2733,30 +2763,6 @@
294 self.overrideEnv('HOME', test_home_dir)
295 self.overrideEnv('BZR_HOME', test_home_dir)
296
297- def setUp(self):
298- super(TestCaseWithMemoryTransport, self).setUp()
299-
300- def _add_disconnect_cleanup(transport):
301- """Schedule disconnection of given transport at test cleanup
302-
303- This needs to happen for all connected transports or leaks occur.
304-
305- Note reconnections may mean we call disconnect multiple times per
306- transport which is suboptimal but seems harmless.
307- """
308- self.addCleanup(transport.disconnect)
309-
310- _mod_transport.Transport.hooks.install_named_hook('post_connect',
311- _add_disconnect_cleanup, None)
312-
313- self._make_test_root()
314- self.addCleanup(os.chdir, os.getcwdu())
315- self.makeAndChdirToTestDir()
316- self.overrideEnvironmentForTesting()
317- self.__readonly_server = None
318- self.__server = None
319- self.reduceLockdirTimeout()
320-
321 def setup_smart_server_with_call_log(self):
322 """Sets up a smart server as the transport server with a call log."""
323 self.transport_server = test_server.SmartTCPServer_for_testing
324@@ -2949,6 +2955,10 @@
325 readwrite one must both define get_url() as resolving to os.getcwd().
326 """
327
328+ def setUp(self):
329+ super(TestCaseWithTransport, self).setUp()
330+ self.__vfs_server = None
331+
332 def get_vfs_only_server(self):
333 """See TestCaseWithMemoryTransport.
334
335@@ -3037,17 +3047,13 @@
336 self.assertFalse(differences.has_changed(),
337 "Trees %r and %r are different: %r" % (left, right, differences))
338
339- def setUp(self):
340- super(TestCaseWithTransport, self).setUp()
341- self.__vfs_server = None
342-
343 def disable_missing_extensions_warning(self):
344 """Some tests expect a precise stderr content.
345
346 There is no point in forcing them to duplicate the extension related
347 warning.
348 """
349- config.GlobalConfig().set_user_option('ignore_missing_extensions', True)
350+ config.GlobalStack().set('ignore_missing_extensions', True)
351
352
353 class ChrootedTestCase(TestCaseWithTransport):
354
355=== modified file 'bzrlib/tests/blackbox/test_init.py'
356--- bzrlib/tests/blackbox/test_init.py 2012-08-04 14:27:47 +0000
357+++ bzrlib/tests/blackbox/test_init.py 2012-08-23 14:01:19 +0000
358@@ -168,10 +168,12 @@
359
360 def test_init_default_format_option(self):
361 """bzr init should read default format from option default_format"""
362- conf = _mod_config.GlobalConfig.from_string('''
363+ g_store = _mod_config.GlobalStore()
364+ g_store._load_from_string('''
365 [DEFAULT]
366 default_format = 1.9
367-''', save=True)
368+''')
369+ g_store.save()
370 out, err = self.run_bzr_subprocess('init')
371 self.assertContainsRe(out, '1.9')
372
373
374=== modified file 'bzrlib/tests/blackbox/test_serve.py'
375--- bzrlib/tests/blackbox/test_serve.py 2012-03-21 13:58:39 +0000
376+++ bzrlib/tests/blackbox/test_serve.py 2012-08-23 14:01:19 +0000
377@@ -274,6 +274,8 @@
378 def test_bzr_serve_supports_configurable_timeout(self):
379 gs = config.GlobalStack()
380 gs.set('serve.client_timeout', 0.2)
381+ # Save the config as the subprocess will use it
382+ gs.store.save()
383 process, url = self.start_server_port()
384 self.build_tree_contents([('a_file', 'contents\n')])
385 # We can connect and issue a request
386@@ -281,9 +283,6 @@
387 self.assertEqual('contents\n', t.get_bytes('a_file'))
388 # However, if we just wait for more content from the server, it will
389 # eventually disconnect us.
390- # TODO: Use something like signal.alarm() so that if the server doesn't
391- # properly handle the timeout, we end up failing the test instead
392- # of hanging forever.
393 m = t.get_smart_medium()
394 m.read_bytes(1)
395 # Now, we wait for timeout to trigger
396
397=== modified file 'bzrlib/tests/test_config.py'
398--- bzrlib/tests/test_config.py 2012-07-28 15:19:25 +0000
399+++ bzrlib/tests/test_config.py 2012-08-23 14:01:19 +0000
400@@ -4347,6 +4347,15 @@
401 self.assertSectionNames(['ALIASES'], self.bazaar_config, 'ALIASES')
402
403
404+class TestSharedStores(tests.TestCaseInTempDir):
405+
406+ def test_bazaar_conf_shared(self):
407+ g1 = config.GlobalStack()
408+ g2 = config.GlobalStack()
409+ # The two stacks share the same store
410+ self.assertIs(g1.store, g2.store)
411+
412+
413 class TestAuthenticationConfigFile(tests.TestCase):
414 """Test the authentication.conf file matching"""
415
416
417=== modified file 'bzrlib/tests/test_debug.py'
418--- bzrlib/tests/test_debug.py 2011-11-16 17:19:13 +0000
419+++ bzrlib/tests/test_debug.py 2012-08-23 14:01:19 +0000
420@@ -26,11 +26,14 @@
421
422 class TestDebugFlags(tests.TestCaseInTempDir):
423
424- def test_set_debug_flags_from_config(self):
425- # test both combinations because configobject automatically splits up
426- # comma-separated lists
427+ def test_set_no_debug_flags_from_config(self):
428+ self.assertDebugFlags([], '')
429+
430+ def test_set_single_debug_flags_from_config(self):
431+ self.assertDebugFlags(['hpss'], 'debug_flags = hpss\n')
432+
433+ def test_set_multiple_debug_flags_from_config(self):
434 self.assertDebugFlags(['hpss', 'error'], 'debug_flags = hpss, error\n')
435- self.assertDebugFlags(['hpss'], 'debug_flags = hpss\n')
436
437 def assertDebugFlags(self, expected_flags, conf_bytes):
438 conf = config.GlobalStack()
439
440=== modified file 'bzrlib/tests/test_lockdir.py'
441--- bzrlib/tests/test_lockdir.py 2012-01-27 22:09:19 +0000
442+++ bzrlib/tests/test_lockdir.py 2012-08-23 14:01:19 +0000
443@@ -734,7 +734,7 @@
444 lambda: 'aproperhostname')
445 # This is off by default at present; see the discussion in the bug.
446 # If you change the default, don't forget to update the docs.
447- config.GlobalConfig().set_user_option('locks.steal_dead', True)
448+ config.GlobalStack().set('locks.steal_dead', True)
449 # Create a lock pretending to come from a different nonexistent
450 # process on the same machine.
451 l1 = LockDir(self.get_transport(), 'a',
452
453=== modified file 'bzrlib/tests/test_ui.py'
454--- bzrlib/tests/test_ui.py 2012-04-30 08:59:57 +0000
455+++ bzrlib/tests/test_ui.py 2012-08-23 14:01:19 +0000
456@@ -56,8 +56,7 @@
457
458 def test_output_encoding_configuration(self):
459 enc = fixtures.generate_unicode_encodings().next()
460- config.GlobalConfig().set_user_option('output_encoding',
461- enc)
462+ config.GlobalStack().set('output_encoding', enc)
463 ui = tests.TestUIFactory(stdin=None,
464 stdout=tests.StringIOWrapper(),
465 stderr=tests.StringIOWrapper())
466
467=== modified file 'doc/en/release-notes/bzr-2.6.txt'
468--- doc/en/release-notes/bzr-2.6.txt 2012-07-28 20:20:38 +0000
469+++ doc/en/release-notes/bzr-2.6.txt 2012-08-23 14:01:19 +0000
470@@ -28,13 +28,17 @@
471 Improvements
472 ************
473
474-``bzr lp-find-proposal`` now only cares about the revision-id that is
475-specified, not the branch you use. This was enabled by a new API call in
476-Launchpad's web service. (Aaron Bentley)
477+* ``bzr lp-find-proposal`` now only cares about the revision-id that is
478+ specified, not the branch you use. This was enabled by a new API call in
479+ Launchpad's web service. (Aaron Bentley)
480
481 * Implement authentication.conf password obfuscation, the password_encoding
482 option can now be set to base64. (Florian Dorn)
483
484+* Local configurations files (i.e. accessed on the local file system like
485+ ``bazaar.conf`` and ``locations.conf``) are now shared, reducing the
486+ number of IOs when querying a configuation option. (Vincent Ladeuil, #832042)
487+
488 Bug Fixes
489 *********
490