Merge lp:~johnsca/charm-helpers/multi-unit into lp:~cf-charmers/charm-helpers/cloud-foundry

Proposed by Cory Johns
Status: Merged
Merged at revision: 183
Proposed branch: lp:~johnsca/charm-helpers/multi-unit
Merge into: lp:~cf-charmers/charm-helpers/cloud-foundry
Diff against target: 312 lines (+104/-81)
4 files modified
charmhelpers/contrib/cloudfoundry/contexts.py (+17/-6)
charmhelpers/core/services.py (+72/-53)
tests/contrib/cloudfoundry/test_render_context.py (+8/-9)
tests/core/test_services.py (+7/-13)
To merge this branch: bzr merge lp:~johnsca/charm-helpers/multi-unit
Reviewer Review Type Date Requested Status
Cloud Foundry Charmers Pending
Review via email: mp+221801@code.launchpad.net

Description of the change

Refactored RelationContext multi-unit support

Added a little magic to the RelationContext to make it better able to support
multiple units and still DWIM in both cases. This might be too magical,
though.

https://codereview.appspot.com/96680043/

To post a comment you must log in.
Revision history for this message
Cory Johns (johnsca) wrote :

Reviewers: mp+221801_code.launchpad.net,

Message:
Please take a look.

Description:
Refactored RelationContext multi-unit support

Added a little magic to the RelationContext to make it better able to
support
multiple units and still DWIM in both cases. This might be too magical,
though.

https://code.launchpad.net/~johnsca/charm-helpers/multi-unit/+merge/221801

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/96680043/

Affected files (+105, -28 lines):
   A [revision details]
   M charmhelpers/contrib/cloudfoundry/contexts.py
   M charmhelpers/core/services.py
   M tests/contrib/cloudfoundry/test_render_context.py
   M tests/core/test_services.py

lp:~johnsca/charm-helpers/multi-unit updated
186. By Cory Johns

Fixed issue with default unit in services framework and improved docs

Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks for this. I include some thoughts below, I hope they are
agreeable.

https://codereview.appspot.com/96680043/diff/20001/charmhelpers/core/services.py
File charmhelpers/core/services.py (right):

https://codereview.appspot.com/96680043/diff/20001/charmhelpers/core/services.py#newcode239
charmhelpers/core/services.py:239:
I make suggestions below on altering this process. Let me know what you
think.

https://codereview.appspot.com/96680043/diff/20001/charmhelpers/core/services.py#newcode298
charmhelpers/core/services.py:298: over the units, not the items of the
default unit.
There is a rule I try to use when generating/process data these days, no
data should live in the key side of the mapping. In the context of a
template for iteration is simpler to iterate

context[interface][{_unit: 'unit/0', 'foo': 'bar'},
                    {_unit: 'unit/1', 'foo': bar}]

This rule makes additional sense when thinking that knowing the unit
names to request them would imply a broken charm and thus shouldn't be
an index in this context. I'd suggest transitioning to that. We could
also make the rule (and document here) that only elements with complete
data appear in the list.

https://codereview.appspot.com/96680043/

Revision history for this message
Cory Johns (johnsca) wrote :

On 2014/06/04 20:57:39, benjamin.saller wrote:

https://codereview.appspot.com/96680043/diff/20001/charmhelpers/core/services.py#newcode298
> charmhelpers/core/services.py:298: over the units, not the items of
the default
> unit.
> There is a rule I try to use when generating/process data these days,
no data
> should live in the key side of the mapping. In the context of a
template for
> iteration is simpler to iterate

> context[interface][{_unit: 'unit/0', 'foo': 'bar'},
> {_unit: 'unit/1', 'foo': bar}]

> This rule makes additional sense when thinking that knowing the unit
names to
> request them would imply a broken charm and thus shouldn't be an index
in this
> context. I'd suggest transitioning to that. We could also make the
rule (and
> document here) that only elements with complete data appear in the
list.

Can you explain your reasoning behind making use of the key? I'm not
averse to changing it to being a list instead of a mapping, but using a
"special" field to hold the unit name ('_unit', in your example) seems
more dangerous, since it could potentially conflict with actual fields,
even if unlikely due to the underscore prefix.

With your comment that the charm shouldn't actually know / care about
the unit name, maybe we can just leave it out altogether, especially if
it only contains "complete" data sets.

https://codereview.appspot.com/96680043/

Revision history for this message
Cory Johns (johnsca) wrote :

That should be: "behind *not* making use of the key"

https://codereview.appspot.com/96680043/

Revision history for this message
Benjamin Saller (bcsaller) wrote :

On 2014/06/04 21:09:21, johnsca wrote:
> On 2014/06/04 20:57:39, benjamin.saller wrote:
> >

https://codereview.appspot.com/96680043/diff/20001/charmhelpers/core/services.py#newcode298
> > charmhelpers/core/services.py:298: over the units, not the items of
the
> default
> > unit.
> > There is a rule I try to use when generating/process data these
days, no data
> > should live in the key side of the mapping. In the context of a
template for
> > iteration is simpler to iterate
> >
> > context[interface][{_unit: 'unit/0', 'foo': 'bar'},
> > {_unit: 'unit/1', 'foo': bar}]
> >
> > This rule makes additional sense when thinking that knowing the unit
names to
> > request them would imply a broken charm and thus shouldn't be an
index in this
> > context. I'd suggest transitioning to that. We could also make the
rule (and
> > document here) that only elements with complete data appear in the
list.

> Can you explain your reasoning behind making use of the key? I'm not
averse to
> changing it to being a list instead of a mapping, but using a
"special" field to
> hold the unit name ('_unit', in your example) seems more dangerous,
since it
> could potentially conflict with actual fields, even if unlikely due to
the
> underscore prefix.

> With your comment that the charm shouldn't actually know / care about
the unit
> name, maybe we can just leave it out altogether, especially if it only
contains
> "complete" data sets.

I think for our use case we don't actually need the unitname at all and
could omit it, in the more general case I expect that there are cases
where people will want to delta the relation data vs previous hook calls
(cached data) before taking some actions.

The list vs dict thing is a pattern that I picked up working in the
machine learning space. It tends to be easier to consume and pivot flat
tuple style data. I was looking for a nice right up justifying this
pattern, its even known as "<someones> law" but I can't remember that
name :-/

However if the value in the key side of the mapping can't be known/used
its a good indicator to me that we can flatten the structure. Maybe not
very persuasive.

https://codereview.appspot.com/96680043/

lp:~johnsca/charm-helpers/multi-unit updated
187. By Cory Johns

Refactored RelationContext to be a bit more intuitive

Revision history for this message
Cory Johns (johnsca) wrote :
lp:~johnsca/charm-helpers/multi-unit updated
188. By Cory Johns

Added mapping iteration methods to RelationContext helper

Revision history for this message
Cory Johns (johnsca) wrote :
lp:~johnsca/charm-helpers/multi-unit updated
189. By Cory Johns

Added ports for log relations and cleaned up other relation keys

Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks for this, very close. Mostly this comes down to me wanting
another pass over the examples/docs around the multi-unit data access.

Hopefully I pointed to what I am after clearly, if not please let me
know.

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/contrib/cloudfoundry/contexts.py
File charmhelpers/contrib/cloudfoundry/contexts.py (right):

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/contrib/cloudfoundry/contexts.py#newcode36
charmhelpers/contrib/cloudfoundry/contexts.py:36: required_keys =
['address', 'port', 'user', 'password']
You have a follow on for the charms updating the templates as well?

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/contrib/cloudfoundry/contexts.py#newcode69
charmhelpers/contrib/cloudfoundry/contexts.py:69: required_keys =
['hostname', 'port']
I am a little cautious about this one as this isn't really a CF
component, it would be interesting if down the road we had something
like

EtcdInterface = charmhelpers.interfaces.Interface('etcd')

To produce this class from a interface registry. We'll want that sort of
thing for the interface testing plans anyway.

just rambling, nothing to do now.

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/core/services.py
File charmhelpers/core/services.py (right):

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/core/services.py#newcode210
charmhelpers/core/services.py:210: """
This feels a bit magical. I'm fine with letting it in but the 'why'
you'd use it both ways should be explained a little more below. See my
following comment about the example being a bit unclear.

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/core/services.py#newcode308
charmhelpers/core/services.py:308: from unit 'wordpress/0'.
I worry that this example isn't clear. self.interface and the 'foo' key
are assumed.

I'd like two examples here in the style of

'if you need to iterate all the units of a relation, for example in a
template, you can ...'

'if you want the keys and values of any matching unit containing all the
expected keys of an interface, you can ...'

sorry to ask you to rephrase this, and thank you.

https://codereview.appspot.com/96680043/

Revision history for this message
Cory Johns (johnsca) wrote :

On 2014/06/06 16:43:12, benjamin.saller wrote:

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/contrib/cloudfoundry/contexts.py#newcode36
> charmhelpers/contrib/cloudfoundry/contexts.py:36: required_keys =
['address',
> 'port', 'user', 'password']
> You have a follow on for the charms updating the templates as well?

Yes, as part of the port conflict resolution. Just testing and hashing
out bugs currently.

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/contrib/cloudfoundry/contexts.py#newcode69
> charmhelpers/contrib/cloudfoundry/contexts.py:69: required_keys =
['hostname',
> 'port']
> I am a little cautious about this one as this isn't really a CF
component, it
> would be interesting if down the road we had something like

> EtcdInterface = charmhelpers.interfaces.Interface('etcd')

> To produce this class from a interface registry. We'll want that sort
of thing
> for the interface testing plans anyway.

I had considered having RelationContext.__init__ accept the interface
name and required keys as parameters so that you don't have to create
stub subclasses, and only need to subclass if you want to add extra
behavior (e.g., the MysqlRelation). I think this is the right way to
go, and will do so.

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/core/services.py#newcode210
> charmhelpers/core/services.py:210: """
> This feels a bit magical. I'm fine with letting it in but the 'why'
you'd use it
> both ways should be explained a little more below. See my following
comment
> about the example being a bit unclear.

I agree. I am very on the fence as to whether to stick with the
"magical" implementation vs creating separate classes, or even just
forcing it to always be a list to force charm authors to always consider
the possibility that there may be more units.

The reason I didn't go with separate classes is because I didn't want to
have to duplicate all the stub RelationContext subclasses, but if we
make it so the interface and keys can be passed in, we can avoid
creating 95% of the stub classes, so that'd be ok.

But now I'm thinking that forcing it to be a list might be better, since
charm authors really should keep in mind that the admin could throw more
units at the relation.

https://codereview.appspot.com/96680043/

lp:~johnsca/charm-helpers/multi-unit updated
190. By Cory Johns

Fixed Cloud Foundry MysqlRelation for new RelationContext data format

191. By Cory Johns

Removed magical default unit behavior from RelationContext

Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks, notes follow.

https://codereview.appspot.com/96680043/diff/100001/charmhelpers/core/services.py
File charmhelpers/core/services.py (right):

https://codereview.appspot.com/96680043/diff/100001/charmhelpers/core/services.py#newcode259
charmhelpers/core/services.py:259: order: 'wordpress/0', 'wordpress/1',
'mediawiki/0'.
I think you do a fine job of explaining this now, but the use-case for
it is still unclear to me.

If there is more than one relation with the same interface I don't think
we can silently combine those lists (the data is coming from different
relation ids). They represent different services and should expect
different orchestration.

I'd go so far as to say this is a whole in our model.

In CF mysql has relations to UAA and CC but doesn't use the services
framework to manage this. If it did and iterated those units uniformly
havoc could follow, no?

The default case isn't this and we built for it, but unless my thinking
here is muddy, we'd need to do something different here. Currently the
relation id information would be lost. We'd at minimum have to put the
rid in the data, or break the units into collections by relation.

Breaking them up by relation is to my mind similar to always processing
this as a list (rather than the default unit notion of before), its the
more complex case, but its one that some services need to be aware of.

I apologize for not picking up on this sooner. If you'd like to talk
about this in the hangout let me know.

https://codereview.appspot.com/96680043/diff/100001/charmhelpers/core/services.py#newcode268
charmhelpers/core/services.py:268: {%- endfor %}
This is good, thanks, it might have to turn into

interface[0][0]['key']

for interface[first relation][first unit][key]

lets talk.

https://codereview.appspot.com/96680043/

lp:~johnsca/charm-helpers/multi-unit updated
192. By Cory Johns

Added doc note about not (currently) preserving relation-id and unit-id info in RelationContext

Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Benjamin Saller (bcsaller) wrote :

+1 LTGM, thanks for talking through that issue.

https://codereview.appspot.com/96680043/diff/120001/charmhelpers/core/services.py
File charmhelpers/core/services.py (right):

https://codereview.appspot.com/96680043/diff/120001/charmhelpers/core/services.py#newcode272
charmhelpers/core/services.py:272: set of data came from, you'll need to
extend this class to preserve
Great, thanks

https://codereview.appspot.com/96680043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmhelpers/contrib/cloudfoundry/contexts.py'
--- charmhelpers/contrib/cloudfoundry/contexts.py 2014-05-26 18:49:24 +0000
+++ charmhelpers/contrib/cloudfoundry/contexts.py 2014-06-06 19:43:53 +0000
@@ -33,7 +33,7 @@
3333
34class NatsRelation(RelationContext):34class NatsRelation(RelationContext):
35 interface = 'nats'35 interface = 'nats'
36 required_keys = ['nats_port', 'nats_address', 'nats_user', 'nats_password']36 required_keys = ['address', 'port', 'user', 'password']
3737
3838
39class MysqlRelation(RelationContext):39class MysqlRelation(RelationContext):
@@ -44,9 +44,10 @@
44 def get_data(self):44 def get_data(self):
45 RelationContext.get_data(self)45 RelationContext.get_data(self)
46 if self.is_ready():46 if self.is_ready():
47 if 'port' not in self['db']:47 for unit in self['db']:
48 self['db']['port'] = '3306'48 if 'port' not in unit:
49 self['db']['dsn'] = self.dsn_template.format(**self['db'])49 unit['port'] = '3306'
50 unit['dsn'] = self.dsn_template.format(**unit)
5051
5152
52class RouterRelation(RelationContext):53class RouterRelation(RelationContext):
@@ -56,9 +57,19 @@
5657
57class LogRouterRelation(RelationContext):58class LogRouterRelation(RelationContext):
58 interface = 'logrouter'59 interface = 'logrouter'
59 required_keys = ['shared-secret', 'logrouter-address']60 required_keys = ['shared_secret', 'address', 'incoming_port', 'outgoing_port']
6061
6162
62class LoggregatorRelation(RelationContext):63class LoggregatorRelation(RelationContext):
63 interface = 'loggregator'64 interface = 'loggregator'
64 required_keys = ['shared_secret', 'loggregator_address']65 required_keys = ['shared_secret', 'address', 'incoming_port', 'outgoing_port']
66
67
68class EtcdRelation(RelationContext):
69 interface = 'etcd'
70 required_keys = ['hostname', 'port']
71
72
73class CloudControllerRelation(RelationContext):
74 interface = 'cc'
75 required_keys = ['hostname', 'port', 'user', 'password']
6576
=== modified file 'charmhelpers/core/services.py'
--- charmhelpers/core/services.py 2014-05-29 17:23:48 +0000
+++ charmhelpers/core/services.py 2014-06-06 19:43:53 +0000
@@ -11,6 +11,14 @@
11 """11 """
12 Register a list of services, given their definitions.12 Register a list of services, given their definitions.
1313
14 Traditional charm authoring is focused on implementing hooks. That is,
15 the charm author is thinking in terms of "What hook am I handling; what
16 does this hook need to do?" However, in most cases, the real question
17 should be "Do I have the information I need to configure and start this
18 piece of software and, if so, what are the steps for doing so." The
19 ServiceManager framework tries to bring the focus to the data and the
20 setup tasks, in the most declarative way possible.
21
14 Service definitions are dicts in the following formats (all keys except22 Service definitions are dicts in the following formats (all keys except
15 'service' are optional):23 'service' are optional):
1624
@@ -67,28 +75,28 @@
67 a mongodb relation and which runs a custom `db_migrate` function prior to75 a mongodb relation and which runs a custom `db_migrate` function prior to
68 restarting the service, and a Runit serivce called spadesd.76 restarting the service, and a Runit serivce called spadesd.
6977
70 >>> manager = services.ServiceManager([78 manager = services.ServiceManager([
71 ... {79 {
72 ... 'service': 'bingod',80 'service': 'bingod',
73 ... 'ports': [80, 443],81 'ports': [80, 443],
74 ... 'required_data': [MongoRelation(), config()],82 'required_data': [MongoRelation(), config(), {'my': 'data'}],
75 ... 'data_ready': [83 'data_ready': [
76 ... services.template(source='bingod.conf'),84 services.template(source='bingod.conf'),
77 ... services.template(source='bingod.ini',85 services.template(source='bingod.ini',
78 ... target='/etc/bingod.ini',86 target='/etc/bingod.ini',
79 ... owner='bingo', perms=0400),87 owner='bingo', perms=0400),
80 ... ],88 ],
81 ... },89 },
82 ... {90 {
83 ... 'service': 'spadesd',91 'service': 'spadesd',
84 ... 'data_ready': services.template(source='spadesd_run.j2',92 'data_ready': services.template(source='spadesd_run.j2',
85 ... target='/etc/sv/spadesd/run',93 target='/etc/sv/spadesd/run',
86 ... perms=0555),94 perms=0555),
87 ... 'start': runit_start,95 'start': runit_start,
88 ... 'stop': runit_stop,96 'stop': runit_stop,
89 ... },97 },
90 ... ])98 ])
91 ... manager.manage()99 manager.manage()
92 """100 """
93 self.services = {}101 self.services = {}
94 for service in services or []:102 for service in services or []:
@@ -222,46 +230,57 @@
222230
223 __nonzero__ = __bool__231 __nonzero__ = __bool__
224232
233 def __repr__(self):
234 return super(RelationContext, self).__repr__()
235
225 def is_ready(self):236 def is_ready(self):
226 """237 """
227 Returns True if all of the required_keys are available.238 Returns True if all of the `required_keys` are available from any units.
228 """239 """
229 return set(self.get(self.interface, {}).keys()).issuperset(set(self.required_keys))240 return len(self.get(self.interface, [])) > 0
241
242 def _is_ready(self, unit_data):
243 """
244 Helper method that tests a set of relation data and returns True if
245 all of the `required_keys` are present.
246 """
247 return set(unit_data.keys()).issuperset(set(self.required_keys))
230248
231 def get_data(self):249 def get_data(self):
232 """250 """
233 Retrieve the relation data and store it under `self[self.interface]`.251 Retrieve the relation data for each unit involved in a realtion and,
234252 if complete, store it in a list under `self[self.interface]`.
235 If there are more than one units related on the desired interface,253
236 then each unit will have its data stored under `self[self.interface][unit_id]`254 The units are sorted lexographically first by the service ID, then by
237 and one of the units with complete information will chosen at random255 the unit ID. Thus, if an interface has two other services, 'db:1'
238 to fill the values at `self[self.interface]`.256 and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
239257 and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
240258 set of data, the relation data for the units will be stored in the
241 For example:259 order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
242260
243 {261 If you only care about a single unit on the relation, you can just
244 'foo': 'bar',262 access it as `{{ interface[0]['key'] }}`. However, if you can at all
245 'unit/0': {263 support multiple units on a relation, you should iterate over the list,
246 'foo': 'bar',264 like:
247 },265
248 'unit/1': {266 {% for unit in interface -%}
249 'foo': 'baz',267 {{ unit['key'] }}{% if not loop.last %},{% endif %}
250 },268 {%- endfor %}
251 }269
270 Note that since all sets of relation data from all related services and
271 units are in a single list, if you need to know which service or unit a
272 set of data came from, you'll need to extend this class to preserve
273 that information.
252 """274 """
253 if not hookenv.relation_ids(self.interface):275 if not hookenv.relation_ids(self.interface):
254 return276 return
255277
256 ns = self.setdefault(self.interface, {})278 ns = self.setdefault(self.interface, [])
257 required = set(self.required_keys)279 for rid in sorted(hookenv.relation_ids(self.interface)):
258 for rid in hookenv.relation_ids(self.interface):280 for unit in sorted(hookenv.related_units(rid)):
259 for unit in hookenv.related_units(rid):
260 reldata = hookenv.relation_get(rid=rid, unit=unit)281 reldata = hookenv.relation_get(rid=rid, unit=unit)
261 unit_ns = ns.setdefault(unit, {})282 if self._is_ready(reldata):
262 unit_ns.update(reldata)283 ns.append(reldata)
263 if set(reldata.keys()).issuperset(required):
264 ns.update(reldata)
265284
266285
267class ManagerCallback(object):286class ManagerCallback(object):
@@ -316,6 +335,6 @@
316335
317336
318# Convenience aliases337# Convenience aliases
319template = TemplateCallback338render_template = template = TemplateCallback
320open_ports = PortManagerCallback()339open_ports = PortManagerCallback()
321close_ports = PortManagerCallback()340close_ports = PortManagerCallback()
322341
=== modified file 'tests/contrib/cloudfoundry/test_render_context.py'
--- tests/contrib/cloudfoundry/test_render_context.py 2014-05-26 18:49:24 +0000
+++ tests/contrib/cloudfoundry/test_render_context.py 2014-06-06 19:43:53 +0000
@@ -18,23 +18,22 @@
18 @mock.patch('charmhelpers.core.hookenv.relation_get')18 @mock.patch('charmhelpers.core.hookenv.relation_get')
19 def test_nats_relation_populated(self, mrel, mid, mrelated):19 def test_nats_relation_populated(self, mrel, mid, mrelated):
20 mid.return_value = ['nats']20 mid.return_value = ['nats']
21 mrel.return_value = {'nats_port': 1234, 'nats_address': 'host',21 mrel.return_value = {'port': 1234, 'address': 'host',
22 'nats_user': 'user', 'nats_password': 'password'}22 'user': 'user', 'password': 'password'}
23 mrelated.return_value = ['router/0']23 mrelated.return_value = ['router/0']
24 n = contexts.NatsRelation()24 n = contexts.NatsRelation()
25 expected = {'nats': {'nats_port': 1234, 'nats_address': 'host',25 expected = {'nats': [{'port': 1234, 'address': 'host',
26 'nats_user': 'user', 'nats_password': 'password',26 'user': 'user', 'password': 'password'}]}
27 'router/0': {'nats_port': 1234, 'nats_address': 'host',
28 'nats_user': 'user', 'nats_password': 'password'}}}
29 self.assertTrue(bool(n))27 self.assertTrue(bool(n))
30 self.assertEqual(n, expected)28 self.assertEqual(n, expected)
29 self.assertEqual(n['nats'][0]['port'], 1234)
3130
32 @mock.patch('charmhelpers.core.hookenv.related_units')31 @mock.patch('charmhelpers.core.hookenv.related_units')
33 @mock.patch('charmhelpers.core.hookenv.relation_ids')32 @mock.patch('charmhelpers.core.hookenv.relation_ids')
34 @mock.patch('charmhelpers.core.hookenv.relation_get')33 @mock.patch('charmhelpers.core.hookenv.relation_get')
35 def test_nats_relation_partial(self, mrel, mid, mrelated):34 def test_nats_relation_partial(self, mrel, mid, mrelated):
36 mid.return_value = ['nats']35 mid.return_value = ['nats']
37 mrel.return_value = {'nats_address': 'host'}36 mrel.return_value = {'address': 'host'}
38 mrelated.return_value = ['router/0']37 mrelated.return_value = ['router/0']
39 n = contexts.NatsRelation()38 n = contexts.NatsRelation()
40 self.assertEqual(n, {})39 self.assertEqual(n, {})
@@ -56,10 +55,10 @@
56 mrel.return_value = {'domain': 'example.com'}55 mrel.return_value = {'domain': 'example.com'}
57 mrelated.return_value = ['router/0']56 mrelated.return_value = ['router/0']
58 n = contexts.RouterRelation()57 n = contexts.RouterRelation()
59 expected = {'router': {'domain': 'example.com',58 expected = {'router': [{'domain': 'example.com'}]}
60 'router/0': {'domain': 'example.com'}}}
61 self.assertTrue(bool(n))59 self.assertTrue(bool(n))
62 self.assertEqual(n, expected)60 self.assertEqual(n, expected)
61 self.assertEqual(n['router'][0]['domain'], 'example.com')
6362
6463
65class TestStoredContext(unittest.TestCase):64class TestStoredContext(unittest.TestCase):
6665
=== modified file 'tests/core/test_services.py'
--- tests/core/test_services.py 2014-05-29 17:23:48 +0000
+++ tests/core/test_services.py 2014-06-06 19:43:53 +0000
@@ -310,7 +310,7 @@
310 mhookenv.related_units.return_value = []310 mhookenv.related_units.return_value = []
311 self.context.get_data()311 self.context.get_data()
312 self.assertFalse(self.context.is_ready())312 self.assertFalse(self.context.is_ready())
313 self.assertEqual(self.context, {'http': {}})313 self.assertEqual(self.context, {'http': []})
314314
315 @mock.patch.object(services, 'hookenv')315 @mock.patch.object(services, 'hookenv')
316 def test_incomplete(self, mhookenv):316 def test_incomplete(self, mhookenv):
@@ -319,8 +319,8 @@
319 mhookenv.relation_get.side_effect = [{}, {'foo': '1'}]319 mhookenv.relation_get.side_effect = [{}, {'foo': '1'}]
320 self.assertFalse(bool(self.context))320 self.assertFalse(bool(self.context))
321 self.assertEqual(mhookenv.relation_get.call_args_list, [321 self.assertEqual(mhookenv.relation_get.call_args_list, [
322 mock.call(rid='apache', unit='apache/0'),
322 mock.call(rid='nginx', unit='nginx/0'),323 mock.call(rid='nginx', unit='nginx/0'),
323 mock.call(rid='apache', unit='apache/0'),
324 ])324 ])
325325
326 @mock.patch.object(services, 'hookenv')326 @mock.patch.object(services, 'hookenv')
@@ -329,23 +329,17 @@
329 mhookenv.related_units.side_effect = lambda i: [i+'/0']329 mhookenv.related_units.side_effect = lambda i: [i+'/0']
330 mhookenv.relation_get.side_effect = [{'foo': '1'}, {'foo': '2', 'bar': '3'}, {}]330 mhookenv.relation_get.side_effect = [{'foo': '1'}, {'foo': '2', 'bar': '3'}, {}]
331 self.context.get_data()331 self.context.get_data()
332 self.assertEqual(self.context, {'http': {332 self.assertTrue(self.context.is_ready())
333 'foo': '2',333 self.assertEqual(self.context, {'http': [
334 'bar': '3',334 {
335 'nginx/0': {
336 'foo': '1',
337 },
338 'apache/0': {
339 'foo': '2',335 'foo': '2',
340 'bar': '3',336 'bar': '3',
341 },337 },
342 'tomcat/0': {338 ]})
343 },
344 }})
345 mhookenv.relation_ids.assert_called_with('http')339 mhookenv.relation_ids.assert_called_with('http')
346 self.assertEqual(mhookenv.relation_get.call_args_list, [340 self.assertEqual(mhookenv.relation_get.call_args_list, [
341 mock.call(rid='apache', unit='apache/0'),
347 mock.call(rid='nginx', unit='nginx/0'),342 mock.call(rid='nginx', unit='nginx/0'),
348 mock.call(rid='apache', unit='apache/0'),
349 mock.call(rid='tomcat', unit='tomcat/0'),343 mock.call(rid='tomcat', unit='tomcat/0'),
350 ])344 ])
351345

Subscribers

People subscribed via source and target branches