Merge lp:~johnsca/charm-helpers/action-metadata-cli-helpers into lp:charm-helpers
- action-metadata-cli-helpers
- Merge into devel
Proposed by
Cory Johns
Status: | Merged |
---|---|
Merged at revision: | 416 |
Proposed branch: | lp:~johnsca/charm-helpers/action-metadata-cli-helpers |
Merge into: | lp:charm-helpers |
Diff against target: |
642 lines (+386/-36) 6 files modified
charmhelpers/cli/__init__.py (+35/-4) charmhelpers/cli/commands.py (+2/-1) charmhelpers/core/hookenv.py (+102/-4) charmhelpers/core/unitdata.py (+61/-17) tests/cli/test_cmdline.py (+56/-9) tests/core/test_hookenv.py (+130/-1) |
To merge this branch: | bzr merge lp:~johnsca/charm-helpers/action-metadata-cli-helpers |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Marco Ceppi | Approve | ||
Review via email:
|
Commit message
Description of the change
Added some new helpers for actions, metadata introspection, and CLI, that became useful when working on charms.reactive.
To post a comment you must log in.
- 418. By Cory Johns
-
Made import of cli subcommands from hookenv explicit
- 419. By Cory Johns
-
Add ability to override unitdata DB file via env, and added unsetrange
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charmhelpers/cli/__init__.py' |
2 | --- charmhelpers/cli/__init__.py 2015-01-22 06:06:03 +0000 |
3 | +++ charmhelpers/cli/__init__.py 2015-07-29 16:32:12 +0000 |
4 | @@ -20,6 +20,8 @@ |
5 | |
6 | from six.moves import zip |
7 | |
8 | +from charmhelpers.core import unitdata |
9 | + |
10 | |
11 | class OutputFormatter(object): |
12 | def __init__(self, outfile=sys.stdout): |
13 | @@ -53,6 +55,8 @@ |
14 | |
15 | def raw(self, output): |
16 | """Output data as raw string (default)""" |
17 | + if isinstance(output, (list, tuple)): |
18 | + output = '\n'.join(map(str, output)) |
19 | self.outfile.write(str(output)) |
20 | |
21 | def py(self, output): |
22 | @@ -91,6 +95,7 @@ |
23 | argument_parser = None |
24 | subparsers = None |
25 | formatter = None |
26 | + exit_code = 0 |
27 | |
28 | def __init__(self): |
29 | if not self.argument_parser: |
30 | @@ -115,6 +120,21 @@ |
31 | return decorated |
32 | return wrapper |
33 | |
34 | + def test_command(self, decorated): |
35 | + """ |
36 | + Subcommand is a boolean test function, so bool return values should be |
37 | + converted to a 0/1 exit code. |
38 | + """ |
39 | + decorated._cli_test_command = True |
40 | + return decorated |
41 | + |
42 | + def no_output(self, decorated): |
43 | + """ |
44 | + Subcommand is not expected to return a value, so don't print a spurious None. |
45 | + """ |
46 | + decorated._cli_no_output = True |
47 | + return decorated |
48 | + |
49 | def subcommand_builder(self, command_name, description=None): |
50 | """ |
51 | Decorate a function that builds a subcommand. Builders should accept a |
52 | @@ -133,11 +153,22 @@ |
53 | argspec = inspect.getargspec(arguments.func) |
54 | vargs = [] |
55 | kwargs = {} |
56 | + for arg in argspec.args: |
57 | + vargs.append(getattr(arguments, arg)) |
58 | if argspec.varargs: |
59 | - vargs = getattr(arguments, argspec.varargs) |
60 | - for arg in argspec.args: |
61 | - kwargs[arg] = getattr(arguments, arg) |
62 | - self.formatter.format_output(arguments.func(*vargs, **kwargs), arguments.format) |
63 | + vargs.extend(getattr(arguments, argspec.varargs)) |
64 | + if argspec.keywords: |
65 | + for kwarg in argspec.keywords.items(): |
66 | + kwargs[kwarg] = getattr(arguments, kwarg) |
67 | + output = arguments.func(*vargs, **kwargs) |
68 | + if getattr(arguments.func, '_cli_test_command', False): |
69 | + self.exit_code = 0 if output else 1 |
70 | + output = '' |
71 | + if getattr(arguments.func, '_cli_no_output', False): |
72 | + output = '' |
73 | + self.formatter.format_output(output, arguments.format) |
74 | + if unitdata._KV: |
75 | + unitdata._KV.flush() |
76 | |
77 | |
78 | cmdline = CommandLine() |
79 | |
80 | === modified file 'charmhelpers/cli/commands.py' |
81 | --- charmhelpers/cli/commands.py 2015-05-13 20:44:19 +0000 |
82 | +++ charmhelpers/cli/commands.py 2015-07-29 16:32:12 +0000 |
83 | @@ -24,8 +24,9 @@ |
84 | from . import CommandLine # noqa |
85 | |
86 | """ |
87 | -Import the sub-modules to be included by chlp. |
88 | +Import the sub-modules which have decorated subcommands to register with chlp. |
89 | """ |
90 | import host # noqa |
91 | import benchmark # noqa |
92 | import unitdata # noqa |
93 | +from charmhelpers.core import hookenv # noqa |
94 | |
95 | === modified file 'charmhelpers/core/hookenv.py' |
96 | --- charmhelpers/core/hookenv.py 2015-07-22 05:58:46 +0000 |
97 | +++ charmhelpers/core/hookenv.py 2015-07-29 16:32:12 +0000 |
98 | @@ -34,6 +34,8 @@ |
99 | import tempfile |
100 | from subprocess import CalledProcessError |
101 | |
102 | +from charmhelpers.cli import cmdline |
103 | + |
104 | import six |
105 | if not six.PY3: |
106 | from UserDict import UserDict |
107 | @@ -173,9 +175,20 @@ |
108 | return os.environ.get('JUJU_RELATION', None) |
109 | |
110 | |
111 | -def relation_id(): |
112 | - """The relation ID for the current relation hook""" |
113 | - return os.environ.get('JUJU_RELATION_ID', None) |
114 | +@cmdline.subcommand() |
115 | +@cached |
116 | +def relation_id(relation_name=None, service_or_unit=None): |
117 | + """The relation ID for the current or a specified relation""" |
118 | + if not relation_name and not service_or_unit: |
119 | + return os.environ.get('JUJU_RELATION_ID', None) |
120 | + elif relation_name and service_or_unit: |
121 | + service_name = service_or_unit.split('/')[0] |
122 | + for relid in relation_ids(relation_name): |
123 | + remote_service = remote_service_name(relid) |
124 | + if remote_service == service_name: |
125 | + return relid |
126 | + else: |
127 | + raise ValueError('Must specify neither or both of relation_name and service_or_unit') |
128 | |
129 | |
130 | def local_unit(): |
131 | @@ -188,14 +201,27 @@ |
132 | return os.environ.get('JUJU_REMOTE_UNIT', None) |
133 | |
134 | |
135 | +@cmdline.subcommand() |
136 | def service_name(): |
137 | """The name service group this unit belongs to""" |
138 | return local_unit().split('/')[0] |
139 | |
140 | |
141 | +@cmdline.subcommand() |
142 | +@cached |
143 | +def remote_service_name(relid=None): |
144 | + """The remote service name for a given relation-id (or the current relation)""" |
145 | + if relid is None: |
146 | + unit = remote_unit() |
147 | + else: |
148 | + units = related_units(relid) |
149 | + unit = units[0] if units else None |
150 | + return unit.split('/')[0] if unit else None |
151 | + |
152 | + |
153 | def hook_name(): |
154 | """The name of the currently executing hook""" |
155 | - return os.path.basename(sys.argv[0]) |
156 | + return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0])) |
157 | |
158 | |
159 | class Config(dict): |
160 | @@ -469,6 +495,63 @@ |
161 | |
162 | |
163 | @cached |
164 | +def relation_to_interface(relation_name): |
165 | + """ |
166 | + Given the name of a relation, return the interface that relation uses. |
167 | + |
168 | + :returns: The interface name, or ``None``. |
169 | + """ |
170 | + return relation_to_role_and_interface(relation_name)[1] |
171 | + |
172 | + |
173 | +@cached |
174 | +def relation_to_role_and_interface(relation_name): |
175 | + """ |
176 | + Given the name of a relation, return the role and the name of the interface |
177 | + that relation uses (where role is one of ``provides``, ``requires``, or ``peer``). |
178 | + |
179 | + :returns: A tuple containing ``(role, interface)``, or ``(None, None)``. |
180 | + """ |
181 | + _metadata = metadata() |
182 | + for role in ('provides', 'requires', 'peer'): |
183 | + interface = _metadata.get(role, {}).get(relation_name, {}).get('interface') |
184 | + if interface: |
185 | + return role, interface |
186 | + return None, None |
187 | + |
188 | + |
189 | +@cached |
190 | +def role_and_interface_to_relations(role, interface_name): |
191 | + """ |
192 | + Given a role and interface name, return a list of relation names for the |
193 | + current charm that use that interface under that role (where role is one |
194 | + of ``provides``, ``requires``, or ``peer``). |
195 | + |
196 | + :returns: A list of relation names. |
197 | + """ |
198 | + _metadata = metadata() |
199 | + results = [] |
200 | + for relation_name, relation in _metadata.get(role, {}).items(): |
201 | + if relation['interface'] == interface_name: |
202 | + results.append(relation_name) |
203 | + return results |
204 | + |
205 | + |
206 | +@cached |
207 | +def interface_to_relations(interface_name): |
208 | + """ |
209 | + Given an interface, return a list of relation names for the current |
210 | + charm that use that interface. |
211 | + |
212 | + :returns: A list of relation names. |
213 | + """ |
214 | + results = [] |
215 | + for role in ('provides', 'requires', 'peer'): |
216 | + results.extend(role_and_interface_to_relations(role, interface_name)) |
217 | + return results |
218 | + |
219 | + |
220 | +@cached |
221 | def charm_name(): |
222 | """Get the name of the current charm as is specified on metadata.yaml""" |
223 | return metadata().get('name') |
224 | @@ -644,6 +727,21 @@ |
225 | subprocess.check_call(['action-fail', message]) |
226 | |
227 | |
228 | +def action_name(): |
229 | + """Get the name of the currently executing action.""" |
230 | + return os.environ.get('JUJU_ACTION_NAME') |
231 | + |
232 | + |
233 | +def action_uuid(): |
234 | + """Get the UUID of the currently executing action.""" |
235 | + return os.environ.get('JUJU_ACTION_UUID') |
236 | + |
237 | + |
238 | +def action_tag(): |
239 | + """Get the tag for the currently executing action.""" |
240 | + return os.environ.get('JUJU_ACTION_TAG') |
241 | + |
242 | + |
243 | def status_set(workload_state, message): |
244 | """Set the workload state with a message |
245 | |
246 | |
247 | === modified file 'charmhelpers/core/unitdata.py' |
248 | --- charmhelpers/core/unitdata.py 2015-03-18 15:51:22 +0000 |
249 | +++ charmhelpers/core/unitdata.py 2015-07-29 16:32:12 +0000 |
250 | @@ -152,6 +152,7 @@ |
251 | import collections |
252 | import contextlib |
253 | import datetime |
254 | +import itertools |
255 | import json |
256 | import os |
257 | import pprint |
258 | @@ -164,8 +165,7 @@ |
259 | class Storage(object): |
260 | """Simple key value database for local unit state within charms. |
261 | |
262 | - Modifications are automatically committed at hook exit. That's |
263 | - currently regardless of exit code. |
264 | + Modifications are not persisted unless :meth:`flush` is called. |
265 | |
266 | To support dicts, lists, integer, floats, and booleans values |
267 | are automatically json encoded/decoded. |
268 | @@ -173,8 +173,11 @@ |
269 | def __init__(self, path=None): |
270 | self.db_path = path |
271 | if path is None: |
272 | - self.db_path = os.path.join( |
273 | - os.environ.get('CHARM_DIR', ''), '.unit-state.db') |
274 | + if 'UNIT_STATE_DB' in os.environ: |
275 | + self.db_path = os.environ['UNIT_STATE_DB'] |
276 | + else: |
277 | + self.db_path = os.path.join( |
278 | + os.environ.get('CHARM_DIR', ''), '.unit-state.db') |
279 | self.conn = sqlite3.connect('%s' % self.db_path) |
280 | self.cursor = self.conn.cursor() |
281 | self.revision = None |
282 | @@ -189,15 +192,8 @@ |
283 | self.conn.close() |
284 | self._closed = True |
285 | |
286 | - def _scoped_query(self, stmt, params=None): |
287 | - if params is None: |
288 | - params = [] |
289 | - return stmt, params |
290 | - |
291 | def get(self, key, default=None, record=False): |
292 | - self.cursor.execute( |
293 | - *self._scoped_query( |
294 | - 'select data from kv where key=?', [key])) |
295 | + self.cursor.execute('select data from kv where key=?', [key]) |
296 | result = self.cursor.fetchone() |
297 | if not result: |
298 | return default |
299 | @@ -206,33 +202,81 @@ |
300 | return json.loads(result[0]) |
301 | |
302 | def getrange(self, key_prefix, strip=False): |
303 | - stmt = "select key, data from kv where key like '%s%%'" % key_prefix |
304 | - self.cursor.execute(*self._scoped_query(stmt)) |
305 | + """ |
306 | + Get a range of keys starting with a common prefix as a mapping of |
307 | + keys to values. |
308 | + |
309 | + :param str key_prefix: Common prefix among all keys |
310 | + :param bool strip: Optionally strip the common prefix from the key |
311 | + names in the returned dict |
312 | + :return dict: A (possibly empty) dict of key-value mappings |
313 | + """ |
314 | + self.cursor.execute("select key, data from kv where key like ?", |
315 | + ['%s%%' % key_prefix]) |
316 | result = self.cursor.fetchall() |
317 | |
318 | if not result: |
319 | - return None |
320 | + return {} |
321 | if not strip: |
322 | key_prefix = '' |
323 | return dict([ |
324 | (k[len(key_prefix):], json.loads(v)) for k, v in result]) |
325 | |
326 | def update(self, mapping, prefix=""): |
327 | + """ |
328 | + Set the values of multiple keys at once. |
329 | + |
330 | + :param dict mapping: Mapping of keys to values |
331 | + :param str prefix: Optional prefix to apply to all keys in `mapping` |
332 | + before setting |
333 | + """ |
334 | for k, v in mapping.items(): |
335 | self.set("%s%s" % (prefix, k), v) |
336 | |
337 | def unset(self, key): |
338 | + """ |
339 | + Remove a key from the database entirely. |
340 | + """ |
341 | self.cursor.execute('delete from kv where key=?', [key]) |
342 | if self.revision and self.cursor.rowcount: |
343 | self.cursor.execute( |
344 | 'insert into kv_revisions values (?, ?, ?)', |
345 | [key, self.revision, json.dumps('DELETED')]) |
346 | |
347 | + def unsetrange(self, keys=None, prefix=""): |
348 | + """ |
349 | + Remove a range of keys starting with a common prefix, from the database |
350 | + entirely. |
351 | + |
352 | + :param list keys: List of keys to remove. |
353 | + :param str prefix: Optional prefix to apply to all keys in ``keys`` |
354 | + before removing. |
355 | + """ |
356 | + if keys is not None: |
357 | + keys = ['%s%s' % (prefix, key) for key in keys] |
358 | + self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys) |
359 | + if self.revision and self.cursor.rowcount: |
360 | + self.cursor.execute( |
361 | + 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)), |
362 | + list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys))) |
363 | + else: |
364 | + self.cursor.execute('delete from kv where key like ?', |
365 | + ['%s%%' % prefix]) |
366 | + if self.revision and self.cursor.rowcount: |
367 | + self.cursor.execute( |
368 | + 'insert into kv_revisions values (?, ?, ?)', |
369 | + ['%s%%' % prefix, self.revision, json.dumps('DELETED')]) |
370 | + |
371 | def set(self, key, value): |
372 | + """ |
373 | + Set a value in the database. |
374 | + |
375 | + :param str key: Key to set the value for |
376 | + :param value: Any JSON-serializable value to be set |
377 | + """ |
378 | serialized = json.dumps(value) |
379 | |
380 | - self.cursor.execute( |
381 | - 'select data from kv where key=?', [key]) |
382 | + self.cursor.execute('select data from kv where key=?', [key]) |
383 | exists = self.cursor.fetchone() |
384 | |
385 | # Skip mutations to the same value |
386 | |
387 | === modified file 'tests/cli/test_cmdline.py' |
388 | --- tests/cli/test_cmdline.py 2014-11-25 15:04:52 +0000 |
389 | +++ tests/cli/test_cmdline.py 2015-07-29 16:32:12 +0000 |
390 | @@ -5,6 +5,7 @@ |
391 | from mock import ( |
392 | patch, |
393 | MagicMock, |
394 | + ANY, |
395 | ) |
396 | import json |
397 | from pprint import pformat |
398 | @@ -87,15 +88,61 @@ |
399 | @self.cl.subcommand() |
400 | def bar(x, y=None, *vargs): |
401 | "A function that does work." |
402 | - self.bar_called = True |
403 | - return "qux" |
404 | - |
405 | - args = ['foo', 'bar', 'baz'] |
406 | - self.cl.formatter = MagicMock() |
407 | - with patch("sys.argv", args): |
408 | - self.cl.run() |
409 | - self.assertTrue(self.bar_called) |
410 | - self.assertTrue(self.cl.formatter.format_output.called) |
411 | + self.assertEqual(x, 'baz') |
412 | + self.assertEqual(y, 'why') |
413 | + self.assertEqual(vargs, ('mux', 'zob')) |
414 | + self.bar_called = True |
415 | + return "qux" |
416 | + |
417 | + args = ['chlp', 'bar', '--y', 'why', 'baz', 'mux', 'zob'] |
418 | + self.cl.formatter = MagicMock() |
419 | + with patch("sys.argv", args): |
420 | + with patch("charmhelpers.core.unitdata._KV") as _KV: |
421 | + self.cl.run() |
422 | + assert _KV.flush.called |
423 | + self.assertTrue(self.bar_called) |
424 | + self.cl.formatter.format_output.assert_called_once_with('qux', ANY) |
425 | + |
426 | + def test_no_output(self): |
427 | + self.bar_called = False |
428 | + |
429 | + @self.cl.subcommand() |
430 | + @self.cl.no_output |
431 | + def bar(x, y=None, *vargs): |
432 | + "A function that does work." |
433 | + self.bar_called = True |
434 | + return "qux" |
435 | + |
436 | + args = ['foo', 'bar', 'baz'] |
437 | + self.cl.formatter = MagicMock() |
438 | + with patch("sys.argv", args): |
439 | + self.cl.run() |
440 | + self.assertTrue(self.bar_called) |
441 | + self.cl.formatter.format_output.assert_called_once_with('', ANY) |
442 | + |
443 | + def test_test_command(self): |
444 | + self.bar_called = False |
445 | + self.bar_result = True |
446 | + |
447 | + @self.cl.subcommand() |
448 | + @self.cl.test_command |
449 | + def bar(x, y=None, *vargs): |
450 | + "A function that does work." |
451 | + self.bar_called = True |
452 | + return self.bar_result |
453 | + |
454 | + args = ['foo', 'bar', 'baz'] |
455 | + self.cl.formatter = MagicMock() |
456 | + with patch("sys.argv", args): |
457 | + self.cl.run() |
458 | + self.assertTrue(self.bar_called) |
459 | + self.assertEqual(self.cl.exit_code, 0) |
460 | + self.cl.formatter.format_output.assert_called_once_with('', ANY) |
461 | + |
462 | + self.bar_result = False |
463 | + with patch("sys.argv", args): |
464 | + self.cl.run() |
465 | + self.assertEqual(self.cl.exit_code, 1) |
466 | |
467 | |
468 | class OutputFormatterTest(TestCase): |
469 | |
470 | === modified file 'tests/core/test_hookenv.py' |
471 | --- tests/core/test_hookenv.py 2015-07-22 05:58:46 +0000 |
472 | +++ tests/core/test_hookenv.py 2015-07-29 16:32:12 +0000 |
473 | @@ -854,14 +854,27 @@ |
474 | 'env': 'some-environment', |
475 | }) |
476 | |
477 | + @patch('charmhelpers.core.hookenv.remote_service_name') |
478 | + @patch('charmhelpers.core.hookenv.relation_ids') |
479 | @patch('charmhelpers.core.hookenv.os') |
480 | - def test_gets_the_relation_id(self, os_): |
481 | + def test_gets_the_relation_id(self, os_, relation_ids, remote_service_name): |
482 | os_.environ = { |
483 | 'JUJU_RELATION_ID': 'foo', |
484 | } |
485 | |
486 | self.assertEqual(hookenv.relation_id(), 'foo') |
487 | |
488 | + relation_ids.return_value = ['r:1', 'r:2'] |
489 | + remote_service_name.side_effect = ['other', 'service'] |
490 | + self.assertEqual(hookenv.relation_id('rel', 'service/0'), 'r:2') |
491 | + relation_ids.assert_called_once_with('rel') |
492 | + self.assertEqual(remote_service_name.call_args_list, [ |
493 | + call('r:1'), |
494 | + call('r:2'), |
495 | + ]) |
496 | + remote_service_name.side_effect = ['other', 'service'] |
497 | + self.assertEqual(hookenv.relation_id('rel', 'service'), 'r:2') |
498 | + |
499 | @patch('charmhelpers.core.hookenv.os') |
500 | def test_relation_id_none_if_no_env(self, os_): |
501 | os_.environ = {} |
502 | @@ -993,6 +1006,7 @@ |
503 | hookenv.relation_set(foo=None) |
504 | check_call_.assert_called_with(['relation-set', 'foo=']) |
505 | |
506 | + @patch('charmhelpers.core.hookenv.local_unit', MagicMock()) |
507 | @patch('os.remove') |
508 | @patch('subprocess.check_output') |
509 | @patch('subprocess.check_call') |
510 | @@ -1021,6 +1035,7 @@ |
511 | self.assertEqual("{foo: bar}", f.read().strip()) |
512 | remove.assert_called_with(temp_file) |
513 | |
514 | + @patch('charmhelpers.core.hookenv.local_unit', MagicMock()) |
515 | @patch('os.remove') |
516 | @patch('subprocess.check_output') |
517 | @patch('subprocess.check_call') |
518 | @@ -1265,6 +1280,20 @@ |
519 | _unit.return_value = 'mysql/3' |
520 | self.assertEqual(hookenv.service_name(), 'mysql') |
521 | |
522 | + @patch('charmhelpers.core.hookenv.related_units') |
523 | + @patch('charmhelpers.core.hookenv.remote_unit') |
524 | + def test_gets_remote_service_name(self, remote_unit, related_units): |
525 | + remote_unit.return_value = 'mysql/3' |
526 | + related_units.return_value = ['pgsql/0', 'pgsql/1'] |
527 | + self.assertEqual(hookenv.remote_service_name(), 'mysql') |
528 | + self.assertEqual(hookenv.remote_service_name('pgsql:1'), 'pgsql') |
529 | + |
530 | + def test_gets_hook_name(self): |
531 | + with patch.dict(os.environ, JUJU_HOOK_NAME='hook'): |
532 | + self.assertEqual(hookenv.hook_name(), 'hook') |
533 | + with patch('sys.argv', ['other-hook']): |
534 | + self.assertEqual(hookenv.hook_name(), 'other-hook') |
535 | + |
536 | @patch('subprocess.check_output') |
537 | def test_action_get_with_key(self, check_output): |
538 | action_data = 'bar' |
539 | @@ -1369,3 +1398,103 @@ |
540 | self.assertTrue(hookenv.has_juju_version('1.24-beta5')) |
541 | self.assertTrue(hookenv.has_juju_version('1.24-beta5.1')) |
542 | self.assertTrue(hookenv.has_juju_version('1.18-backport6')) |
543 | + |
544 | + @patch.object(hookenv, 'relation_to_role_and_interface') |
545 | + def test_relation_to_interface(self, rtri): |
546 | + rtri.return_value = (None, 'foo') |
547 | + self.assertEqual(hookenv.relation_to_interface('rel'), 'foo') |
548 | + |
549 | + @patch.object(hookenv, 'metadata') |
550 | + def test_relation_to_role_and_interface(self, metadata): |
551 | + metadata.return_value = { |
552 | + 'provides': { |
553 | + 'pro-rel': { |
554 | + 'interface': 'pro-int', |
555 | + }, |
556 | + 'pro-rel2': { |
557 | + 'interface': 'pro-int', |
558 | + }, |
559 | + }, |
560 | + 'requires': { |
561 | + 'req-rel': { |
562 | + 'interface': 'req-int', |
563 | + }, |
564 | + }, |
565 | + 'peer': { |
566 | + 'pee-rel': { |
567 | + 'interface': 'pee-int', |
568 | + }, |
569 | + }, |
570 | + } |
571 | + rtri = hookenv.relation_to_role_and_interface |
572 | + self.assertEqual(rtri('pro-rel'), ('provides', 'pro-int')) |
573 | + self.assertEqual(rtri('req-rel'), ('requires', 'req-int')) |
574 | + self.assertEqual(rtri('pee-rel'), ('peer', 'pee-int')) |
575 | + |
576 | + @patch.object(hookenv, 'metadata') |
577 | + def test_role_and_interface_to_relations(self, metadata): |
578 | + metadata.return_value = { |
579 | + 'provides': { |
580 | + 'pro-rel': { |
581 | + 'interface': 'pro-int', |
582 | + }, |
583 | + 'pro-rel2': { |
584 | + 'interface': 'pro-int', |
585 | + }, |
586 | + }, |
587 | + 'requires': { |
588 | + 'req-rel': { |
589 | + 'interface': 'int', |
590 | + }, |
591 | + }, |
592 | + 'peer': { |
593 | + 'pee-rel': { |
594 | + 'interface': 'int', |
595 | + }, |
596 | + }, |
597 | + } |
598 | + ritr = hookenv.role_and_interface_to_relations |
599 | + assertItemsEqual = getattr(self, 'assertItemsEqual', getattr(self, 'assertCountEqual', None)) |
600 | + assertItemsEqual(ritr('provides', 'pro-int'), ['pro-rel', 'pro-rel2']) |
601 | + assertItemsEqual(ritr('requires', 'int'), ['req-rel']) |
602 | + assertItemsEqual(ritr('peer', 'int'), ['pee-rel']) |
603 | + |
604 | + @patch.object(hookenv, 'metadata') |
605 | + def test_interface_to_relations(self, metadata): |
606 | + metadata.return_value = { |
607 | + 'provides': { |
608 | + 'pro-rel': { |
609 | + 'interface': 'pro-int', |
610 | + }, |
611 | + 'pro-rel2': { |
612 | + 'interface': 'pro-int', |
613 | + }, |
614 | + }, |
615 | + 'requires': { |
616 | + 'req-rel': { |
617 | + 'interface': 'req-int', |
618 | + }, |
619 | + }, |
620 | + 'peer': { |
621 | + 'pee-rel': { |
622 | + 'interface': 'pee-int', |
623 | + }, |
624 | + }, |
625 | + } |
626 | + itr = hookenv.interface_to_relations |
627 | + assertItemsEqual = getattr(self, 'assertItemsEqual', getattr(self, 'assertCountEqual', None)) |
628 | + assertItemsEqual(itr('pro-int'), ['pro-rel', 'pro-rel2']) |
629 | + assertItemsEqual(itr('req-int'), ['req-rel']) |
630 | + assertItemsEqual(itr('pee-int'), ['pee-rel']) |
631 | + |
632 | + def test_action_name(self): |
633 | + with patch.dict('os.environ', JUJU_ACTION_NAME='action-jack'): |
634 | + self.assertEqual(hookenv.action_name(), 'action-jack') |
635 | + |
636 | + def test_action_uuid(self): |
637 | + with patch.dict('os.environ', JUJU_ACTION_UUID='action-jack'): |
638 | + self.assertEqual(hookenv.action_uuid(), 'action-jack') |
639 | + |
640 | + def test_action_tag(self): |
641 | + with patch.dict('os.environ', JUJU_ACTION_TAG='action-jack'): |
642 | + self.assertEqual(hookenv.action_tag(), 'action-jack') |
LGTM