Merge lp:~mew/charm-helpers/refactor-to-core into lp:charm-helpers

Proposed by Matthew Wedgwood
Status: Merged
Merged at revision: 12
Proposed branch: lp:~mew/charm-helpers/refactor-to-core
Merge into: lp:charm-helpers
Diff against target: 1381 lines (+395/-421)
14 files modified
README.test (+3/-0)
charmhelpers/contrib/charmhelpers/__init__.py (+41/-135)
charmhelpers/contrib/charmsupport/nrpe.py (+8/-2)
charmhelpers/contrib/jujugui/utils.py (+9/-8)
charmhelpers/core/hookenv.py (+60/-7)
charmhelpers/core/host.py (+1/-1)
charmhelpers/fetch/__init__.py (+46/-0)
charmhelpers/payload/__init__.py (+1/-0)
charmhelpers/payload/execd.py (+18/-13)
tests/contrib/charmhelpers/test_charmhelpers.py (+0/-184)
tests/contrib/jujugui/test_utils.py (+6/-10)
tests/core/test_hookenv.py (+146/-41)
tests/core/test_host.py (+2/-3)
tests/payload/test_execd.py (+54/-17)
To merge this branch: bzr merge lp:~mew/charm-helpers/refactor-to-core
Reviewer Review Type Date Requested Status
Francesco Banconi (community) Approve
Brad Crittenden (community) code Approve
Charm Helper Maintainers Pending
Review via email: mp+164980@code.launchpad.net

Commit message

Refactor charmsupport and charmhelpers(legacy) code in to core modules for charmhelpers

Description of the change

This doozy of an MP moves the bulk of library code into a main module structure:

core: low-level code for interacting with the charm environment
  hookenv: work with the charm's juju environment
  host: perform common configuration actions on the host
fetch: work with external sources such as apt, Launchpad, Github, and others
payload: work with charm payloads (code injected before deployment)

This release is NOT stable, as (for example) some code in host.py will eventually land in the fetch module.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Matthew,

Thanks for doing this reorganization. Lots of work and gets the project moving.

I couldn't get 'make test' to run until I installed
python-nose
python-mock
python-testtools

Could you add those to README.test?

The temporary mapping in __ALL__ for stuff moved from charmhelpers/__init__.py is a nice touch.

A lot of the things you marked 'Deprecated: client-only' are functions that we used in testing our charms. They are actually quite nice. I'd like to see the 'Deprecation' tag removed and try to find a place to put them. If you'd like to see them in use have a look at the buildbot-master charm.

Overall I like this direction.

review: Approve (code)
Revision history for this message
Matthew Wedgwood (mew) wrote :

Those client-only pieces don't really fit the objectives of charm-helpers to be a framework/library for use inside charms. I agree that they are useful, but they should live outside the project (probably in charm-tools).

20. By Matthew Wedgwood

[agy] Fix upstart detection, initscript tests, and README (for testing)

Revision history for this message
Francesco Banconi (frankban) wrote :

This is a very nice incremental step Matthew, thank you.

The tests runs well.
"make lint" requires flake8 to be installed (I did it with "sudo pip install flake8").
There are several lint/pep8 failures, and I understand that fixing them is not the goal of this branch.
For this reason feel free to ignore the minors below.

267 + config,
268 + local_unit,
269 + log,
270 + relation_ids,
271 + relation_set,
272 + )
Minor: these lines could be dedented.

395 + for key in ('provides','requires','peers'):
Missing whitespace after ','.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README.test'
2--- README.test 2013-05-13 13:06:50 +0000
3+++ README.test 2013-05-23 20:50:31 +0000
4@@ -2,3 +2,6 @@
5 -----------------------------------
6 python-shelltoolbox
7 python-tempita
8+python-nose
9+python-mock
10+python-testtools
11
12=== modified file 'charmhelpers/contrib/charmhelpers/__init__.py'
13--- charmhelpers/contrib/charmhelpers/__init__.py 2013-05-11 20:25:54 +0000
14+++ charmhelpers/contrib/charmhelpers/__init__.py 2013-05-23 20:50:31 +0000
15@@ -1,160 +1,60 @@
16 # Copyright 2012 Canonical Ltd. This software is licensed under the
17 # GNU Affero General Public License version 3 (see the file LICENSE).
18
19+import warnings
20+warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning)
21+
22 """Helper functions for writing Juju charms in Python."""
23
24 __metaclass__ = type
25-__all__ = ['get_config',
26- 'log',
27- 'log_entry',
28- 'log_exit',
29- 'relation_get',
30- 'relation_set',
31- 'relation_ids',
32- 'relation_list',
33- 'config_get',
34- 'unit_get',
35- 'open_port',
36- 'close_port',
37- 'service_control',
38- 'unit_info',
39- 'wait_for_machine',
40- 'wait_for_page_contents',
41- 'wait_for_relation',
42- 'wait_for_unit',
43+__all__ = [
44+ #'get_config', # core.hookenv.config()
45+ #'log', # core.hookenv.log()
46+ #'log_entry', # core.hookenv.log()
47+ #'log_exit', # core.hookenv.log()
48+ #'relation_get', # core.hookenv.relation_get()
49+ #'relation_set', # core.hookenv.relation_set()
50+ #'relation_ids', # core.hookenv.relation_ids()
51+ #'relation_list', # core.hookenv.relation_units()
52+ #'config_get', # core.hookenv.config()
53+ #'unit_get', # core.hookenv.unit_get()
54+ #'open_port', # core.hookenv.open_port()
55+ #'close_port', # core.hookenv.close_port()
56+ #'service_control', # core.host.service()
57+ 'unit_info', # client-side, NOT IMPLEMENTED
58+ 'wait_for_machine', # client-side, NOT IMPLEMENTED
59+ 'wait_for_page_contents', # client-side, NOT IMPLEMENTED
60+ 'wait_for_relation', # client-side, NOT IMPLEMENTED
61+ 'wait_for_unit', # client-side, NOT IMPLEMENTED
62 ]
63
64-from collections import namedtuple
65-import json
66 import operator
67 from shelltoolbox import (
68 command,
69- script_name,
70- run,
71 )
72 import tempfile
73 import time
74 import urllib2
75 import yaml
76-from subprocess import CalledProcessError
77-
78
79 SLEEP_AMOUNT = 0.1
80-Env = namedtuple('Env', 'uid gid home')
81 # We create a juju_status Command here because it makes testing much,
82 # much easier.
83 juju_status = lambda: command('juju')('status')
84
85-
86-def log(message, juju_log=command('juju-log')):
87- return juju_log('--', message)
88-
89-
90-def log_entry():
91- log("--> Entering {}".format(script_name()))
92-
93-
94-def log_exit():
95- log("<-- Exiting {}".format(script_name()))
96-
97-
98-def get_config():
99- _config_get = command('config-get', '--format=json')
100- return json.loads(_config_get())
101-
102-
103-def relation_get(attribute=None, unit=None, rid=None):
104- cmd = command('relation-get')
105- if attribute is None and unit is None and rid is None:
106- return cmd().strip()
107- _args = []
108- if rid:
109- _args.append('-r')
110- _args.append(rid)
111- if attribute is not None:
112- _args.append(attribute)
113- if unit:
114- _args.append(unit)
115- return cmd(*_args).strip()
116-
117-
118-def relation_set(**kwargs):
119- cmd = command('relation-set')
120- args = ['{}={}'.format(k, v) for k, v in kwargs.items()]
121- cmd(*args)
122-
123-
124-def relation_ids(relation_name):
125- cmd = command('relation-ids')
126- args = [relation_name]
127- return cmd(*args).split()
128-
129-
130-def relation_list(rid=None):
131- cmd = command('relation-list')
132- args = []
133- if rid:
134- args.append('-r')
135- args.append(rid)
136- return cmd(*args).split()
137-
138-
139-def config_get(attribute):
140- cmd = command('config-get')
141- args = [attribute]
142- return cmd(*args).strip()
143-
144-
145-def unit_get(attribute):
146- cmd = command('unit-get')
147- args = [attribute]
148- return cmd(*args).strip()
149-
150-
151-def open_port(port, protocol="TCP"):
152- cmd = command('open-port')
153- args = ['{}/{}'.format(port, protocol)]
154- cmd(*args)
155-
156-
157-def close_port(port, protocol="TCP"):
158- cmd = command('close-port')
159- args = ['{}/{}'.format(port, protocol)]
160- cmd(*args)
161-
162-START = "start"
163-RESTART = "restart"
164-STOP = "stop"
165-RELOAD = "reload"
166-
167-
168-def service_control(service_name, action):
169- cmd = command('service')
170- args = [service_name, action]
171- try:
172- if action == RESTART:
173- try:
174- cmd(*args)
175- except CalledProcessError:
176- service_control(service_name, START)
177- else:
178- cmd(*args)
179- except CalledProcessError:
180- log("Failed to perform {} on service {}".format(action, service_name))
181-
182-
183-def configure_source(update=False):
184- source = config_get('source')
185- if ((source.startswith('ppa:') or
186- source.startswith('cloud:') or
187- source.startswith('http:'))):
188- run('add-apt-repository', source)
189- if source.startswith("http:"):
190- run('apt-key', 'import', config_get('key'))
191- if update:
192- run('apt-get', 'update')
193-
194-
195+# re-implemented as charmhelpers.fetch.configure_sources()
196+#def configure_source(update=False):
197+# source = config_get('source')
198+# if ((source.startswith('ppa:') or
199+# source.startswith('cloud:') or
200+# source.startswith('http:'))):
201+# run('add-apt-repository', source)
202+# if source.startswith("http:"):
203+# run('apt-key', 'import', config_get('key'))
204+# if update:
205+# run('apt-get', 'update')
206+
207+# DEPRECATED: client-side only
208 def make_charm_config_file(charm_config):
209 charm_config_file = tempfile.NamedTemporaryFile()
210 charm_config_file.write(yaml.dump(charm_config))
211@@ -165,6 +65,7 @@
212 return charm_config_file
213
214
215+# DEPRECATED: client-side only
216 def unit_info(service_name, item_name, data=None, unit=None):
217 if data is None:
218 data = yaml.safe_load(juju_status())
219@@ -189,10 +90,12 @@
220 return item
221
222
223+# DEPRECATED: client-side only
224 def get_machine_data():
225 return yaml.safe_load(juju_status())['machines']
226
227
228+# DEPRECATED: client-side only
229 def wait_for_machine(num_machines=1, timeout=300):
230 """Wait `timeout` seconds for `num_machines` machines to come up.
231
232@@ -232,6 +135,7 @@
233 return num_machines, time.time() - start_time
234
235
236+# DEPRECATED: client-side only
237 def wait_for_unit(service_name, timeout=480):
238 """Wait `timeout` seconds for a given service name to come up."""
239 wait_for_machine(num_machines=1)
240@@ -247,6 +151,7 @@
241 raise RuntimeError('unit did not start, agent-state: ' + state)
242
243
244+# DEPRECATED: client-side only
245 def wait_for_relation(service_name, relation_name, timeout=120):
246 """Wait `timeout` seconds for a given relation to come up."""
247 start_time = time.time()
248@@ -259,6 +164,7 @@
249 time.sleep(SLEEP_AMOUNT)
250
251
252+# DEPRECATED: client-side only
253 def wait_for_page_contents(url, contents, timeout=120, validate=None):
254 if validate is None:
255 validate = operator.contains
256
257=== modified file 'charmhelpers/contrib/charmsupport/nrpe.py'
258--- charmhelpers/contrib/charmsupport/nrpe.py 2013-05-11 20:05:13 +0000
259+++ charmhelpers/contrib/charmsupport/nrpe.py 2013-05-23 20:50:31 +0000
260@@ -12,8 +12,14 @@
261 import shlex
262 import yaml
263
264-from hookenv import config, local_unit, log, relation_ids, relation_set
265-from host import service
266+from charmhelpers.core.hookenv import (
267+ config,
268+ local_unit,
269+ log,
270+ relation_ids,
271+ relation_set,
272+ )
273+from charmhelpers.core.host import service
274
275 # This module adds compatibility with the nrpe-external-master and plain nrpe
276 # subordinate charms. To use it in your charm:
277
278=== modified file 'charmhelpers/contrib/jujugui/utils.py'
279--- charmhelpers/contrib/jujugui/utils.py 2013-05-11 20:39:53 +0000
280+++ charmhelpers/contrib/jujugui/utils.py 2013-05-23 20:50:31 +0000
281@@ -64,11 +64,12 @@
282 search_file,
283 su,
284 )
285-from charmhelpers.contrib.charmhelpers import (
286- START,
287- get_config,
288+from charmhelpers.core.host import (
289+ service_start,
290+)
291+from charmhelpers.core.hookenv import (
292 log,
293- service_control,
294+ config,
295 unit_get,
296 )
297
298@@ -263,9 +264,9 @@
299 global results_log
300 if results_log is not None:
301 return
302- config = get_config()
303+ cfg = config()
304 logging.basicConfig(
305- filename=config['command-log-file'],
306+ filename=cfg['command-log-file'],
307 level=logging.INFO,
308 format="%(asctime)s: %(name)s@%(levelname)s %(message)s")
309 results_log = logging.getLogger('juju-gui')
310@@ -295,7 +296,7 @@
311 render_to_file('config/juju-api-improv.conf.template', context, config_path)
312 log('Starting the staging backend.')
313 with su('root'):
314- service_control(IMPROV, START)
315+ service_start(IMPROV)
316
317
318 def start_agent(
319@@ -317,7 +318,7 @@
320 render_to_file('config/juju-api-agent.conf.template', context, config_path)
321 log('Starting API agent.')
322 with su('root'):
323- service_control(AGENT, START)
324+ service_start(AGENT)
325
326
327 def start_gui(
328
329=== added directory 'charmhelpers/core'
330=== added file 'charmhelpers/core/__init__.py'
331=== renamed file 'charmhelpers/contrib/charmsupport/hookenv.py' => 'charmhelpers/core/hookenv.py'
332--- charmhelpers/contrib/charmsupport/hookenv.py 2013-05-11 20:24:29 +0000
333+++ charmhelpers/core/hookenv.py 2013-05-23 20:50:31 +0000
334@@ -1,8 +1,8 @@
335 "Interactions with the Juju environment"
336-# Copyright 2012 Canonical Ltd.
337+# Copyright 2013 Canonical Ltd.
338 #
339 # Authors:
340-# Matthew Wedgwood <matthew.wedgwood@canonical.com>
341+# Charm Helpers Developers <juju@lists.ubuntu.com>
342
343 import os
344 import json
345@@ -62,8 +62,11 @@
346 """A convenient bundling of the current execution context"""
347 context = {}
348 context['conf'] = config()
349+ context['reltype'] = relation_type()
350+ context['relid'] = relation_id()
351 context['unit'] = local_unit()
352- context['rel'] = relations_of_type()
353+ context['rels'] = relations()
354+ context['rel'] = relation_get()
355 context['env'] = os.environ
356 return context
357
358@@ -112,11 +115,13 @@
359 if rid:
360 _args.append('-r')
361 _args.append(rid)
362- if attribute is not None:
363- _args.append(attribute)
364+ _args.append(attribute or '-')
365 if unit:
366 _args.append(unit)
367- return json.loads(subprocess.check_output(_args))
368+ try:
369+ return json.loads(subprocess.check_output(_args))
370+ except ValueError:
371+ return None
372
373
374 def relation_set(relation_id=None, **kwargs):
375@@ -134,7 +139,8 @@
376 relid_cmd_line = ['relation-ids', '--format=json']
377 if reltype is not None:
378 relid_cmd_line.append(reltype)
379- return json.loads(subprocess.check_output(relid_cmd_line))
380+ return json.loads(subprocess.check_output(relid_cmd_line))
381+ return []
382
383
384 def related_units(relid=None):
385@@ -179,6 +185,53 @@
386 return relation_data
387
388
389+def relation_types():
390+ "Get a list of relation types supported by this charm"
391+ charmdir = os.environ.get('CHARM_DIR', '')
392+ mdf = open(os.path.join(charmdir, 'metadata.yaml'))
393+ md = yaml.safe_load(mdf)
394+ rel_types = []
395+ for key in ('provides','requires','peers'):
396+ section = md.get(key)
397+ if section:
398+ rel_types.extend(section.keys())
399+ mdf.close()
400+ return rel_types
401+
402+
403+def relations():
404+ rels = {}
405+ for reltype in relation_types():
406+ relids = {}
407+ for relid in relation_ids(reltype):
408+ units = {}
409+ for unit in related_units(relid):
410+ reldata = relation_get(unit=unit, rid=relid)
411+ units[unit] = reldata
412+ relids[relid] = units
413+ rels[reltype] = relids
414+ return rels
415+
416+
417+def open_port(port, protocol="TCP"):
418+ "Open a service network port"
419+ _args = ['open-port']
420+ _args.append('{}/{}'.format(port, protocol))
421+ subprocess.check_call(_args)
422+
423+
424+def close_port(port, protocol="TCP"):
425+ "Close a service network port"
426+ _args = ['close-port']
427+ _args.append('{}/{}'.format(port, protocol))
428+ subprocess.check_call(_args)
429+
430+
431+def unit_get(attribute):
432+ _args = ['unit-get', attribute]
433+ return subprocess.check_output(_args).strip()
434+
435+
436 class UnregisteredHookError(Exception):
437 pass
438
439
440=== renamed file 'charmhelpers/contrib/charmsupport/host.py' => 'charmhelpers/core/host.py'
441--- charmhelpers/contrib/charmsupport/host.py 2013-05-11 20:05:13 +0000
442+++ charmhelpers/core/host.py 2013-05-23 20:50:31 +0000
443@@ -23,7 +23,7 @@
444
445 def service(action, service_name):
446 cmd = None
447- if os.path.exists(os.path.join('/etc/init', service_name)):
448+ if os.path.exists(os.path.join('/etc/init', '%s.conf' % service_name)):
449 cmd = ['initctl', action, service_name]
450 elif os.path.exists(os.path.join('/etc/init.d', service_name)):
451 cmd = [os.path.join('/etc/init.d', service_name), action]
452
453=== added directory 'charmhelpers/fetch'
454=== added file 'charmhelpers/fetch/__init__.py'
455--- charmhelpers/fetch/__init__.py 1970-01-01 00:00:00 +0000
456+++ charmhelpers/fetch/__init__.py 2013-05-23 20:50:31 +0000
457@@ -0,0 +1,46 @@
458+from yaml import safe_load
459+from core.hookenv import config_get
460+from subprocess import check_call
461+
462+
463+def add_source(source, key=None):
464+ if ((source.startswith('ppa:') or
465+ source.startswith('cloud:') or
466+ source.startswith('http:'))):
467+ check_call('add-apt-repository', source)
468+ if key:
469+ check_call('apt-key', 'import', key)
470+
471+
472+class SourceConfigError(Exception):
473+ pass
474+
475+
476+def configure_sources(update=False,
477+ sources_var='install_sources',
478+ keys_var='install_keys'):
479+ """
480+ Configure multiple sources from charm configuration
481+
482+ Example config:
483+ install_sources:
484+ - "ppa:foo"
485+ - "http://example.com/repo precise main"
486+ install_keys:
487+ - null
488+ - "a1b2c3d4"
489+
490+ Note that 'null' (a.k.a. None) should not be quoted.
491+ """
492+ sources = safe_load(config_get(sources_var))
493+ keys = safe_load(config_get(keys_var))
494+ if isinstance(sources, basestring) and isinstance(keys, basestring):
495+ add_source(sources, keys)
496+ else:
497+ if not len(sources) == len(keys):
498+ msg = 'Install sources and keys lists are different lengths'
499+ raise SourceConfigError(msg)
500+ for src_num in range(len(sources)):
501+ add_source(sources[src_num], sources[src_num])
502+ if update:
503+ check_call(('apt-get', 'update'))
504
505=== added directory 'charmhelpers/payload'
506=== added file 'charmhelpers/payload/__init__.py'
507--- charmhelpers/payload/__init__.py 1970-01-01 00:00:00 +0000
508+++ charmhelpers/payload/__init__.py 2013-05-23 20:50:31 +0000
509@@ -0,0 +1,1 @@
510+"Tools for working with files injected into a charm just before deployment."
511
512=== renamed file 'charmhelpers/contrib/charmsupport/execd.py' => 'charmhelpers/payload/execd.py'
513--- charmhelpers/contrib/charmsupport/execd.py 2013-05-11 20:05:13 +0000
514+++ charmhelpers/payload/execd.py 2013-05-23 20:50:31 +0000
515@@ -3,32 +3,37 @@
516 import os
517 import sys
518 import subprocess
519-import hookenv
520+from charmhelpers.core import hookenv
521
522
523 def default_execd_dir():
524 return os.path.join(os.environ['CHARM_DIR'],'exec.d')
525
526
527-def execd_modules(execd_dir=None):
528+def execd_module_paths(execd_dir=None):
529 if not execd_dir:
530 execd_dir = default_execd_dir()
531 for subpath in os.listdir(execd_dir):
532 module = os.path.join(execd_dir, subpath)
533 if os.path.isdir(module):
534- yield module
535-
536-
537-def execd_run(submodule, execd_dir=None, die_on_error=False):
538- for module_path in execd_modules(execd_dir):
539+ yield module
540+
541+
542+def execd_submodule_paths(submodule, execd_dir=None):
543+ for module_path in execd_module_paths(execd_dir):
544 path = os.path.join(module_path, submodule)
545 if os.access(path, os.X_OK) and os.path.isfile(path):
546- try:
547- subprocess.check_call(path, shell=True)
548- except subprocess.CalledProcessError as e:
549- hookenv.log(e.output)
550- if die_on_error:
551- sys.exit(e.returncode)
552+ yield path
553+
554+
555+def execd_run(submodule, execd_dir=None, die_on_error=False):
556+ for submodule_path in execd_submodule_paths(submodule, execd_dir):
557+ try:
558+ subprocess.check_call(submodule_path, shell=True)
559+ except subprocess.CalledProcessError as e:
560+ hookenv.log(e.output)
561+ if die_on_error:
562+ sys.exit(e.returncode)
563
564
565 def execd_preinstall(execd_dir=None):
566
567=== modified file 'tests/contrib/charmhelpers/test_charmhelpers.py'
568--- tests/contrib/charmhelpers/test_charmhelpers.py 2013-05-11 20:30:34 +0000
569+++ tests/contrib/charmhelpers/test_charmhelpers.py 2013-05-23 20:50:31 +0000
570@@ -3,7 +3,6 @@
571 import unittest
572 import yaml
573
574-from simplejson import dumps
575 from StringIO import StringIO
576 from testtools import TestCase
577
578@@ -14,8 +13,6 @@
579 sys.path.insert(0, 'helpers/python')
580 from charmhelpers.contrib import charmhelpers
581
582-from subprocess import CalledProcessError
583-
584
585 class CharmHelpersTestCase(TestCase):
586 """A basic test case for Python charm helpers."""
587@@ -73,145 +70,6 @@
588 self._make_juju_status_dict(
589 num_units, service_name, unit_state, machine_state))
590
591- def test_get_config(self):
592- # get_config returns the contents of the current charm
593- # configuration, as returned by config-get --format=json.
594- mock_config = {'key': 'value'}
595-
596- # Monkey-patch shelltoolbox.command to avoid having to call out
597- # to config-get.
598- self._patch_command(lambda: dumps(mock_config))
599- self.assertEqual(mock_config, charmhelpers.get_config())
600-
601- def test_config_get(self):
602- # config_get is used to retrieve individual configuration elements
603- mock_config = {'key': 'value'}
604-
605- # Monkey-patch shelltoolbox.command to avoid having to call out
606- # to config-get.
607- self._patch_command(lambda *args: mock_config[args[0]])
608- self.assertEqual(mock_config['key'], charmhelpers.config_get('key'))
609-
610- def test_unit_get(self):
611- # unit_get is used to retrieve individual configuration elements
612- mock_config = {'key': 'value'}
613-
614- # Monkey-patch shelltoolbox.command to avoid having to call out
615- # to unit-get.
616- self._patch_command(lambda *args: mock_config[args[0]])
617- self.assertEqual(mock_config['key'], charmhelpers.unit_get('key'))
618-
619- def test_relation_get(self):
620- # relation_get returns the value of a given relation variable,
621- # as returned by relation-get $VAR.
622- mock_relation_values = {
623- 'foo': 'bar',
624- 'spam': 'eggs'}
625- self._patch_command(lambda *args: mock_relation_values[args[0]])
626- self.assertEqual('bar', charmhelpers.relation_get('foo'))
627- self.assertEqual('eggs', charmhelpers.relation_get('spam'))
628-
629- self._patch_command(lambda *args: mock_relation_values[args[2]])
630- self.assertEqual('bar',
631- charmhelpers.relation_get('foo', 'test', 'test:1'))
632- self.assertEqual('eggs',
633- charmhelpers.relation_get('spam', 'test', 'test:1'))
634-
635- self._patch_command(lambda *args: '%s' % mock_relation_values)
636- self.assertEqual("{'foo': 'bar', 'spam': 'eggs'}",
637- charmhelpers.relation_get())
638-
639- def test_relation_set(self):
640- # relation_set calls out to relation-set and passes key=value
641- # pairs to it.
642- items_set = {}
643-
644- def mock_relation_set(*args):
645- for arg in args:
646- key, value = arg.split("=")
647- items_set[key] = value
648- self._patch_command(mock_relation_set)
649- charmhelpers.relation_set(foo='bar', spam='eggs')
650- self.assertEqual('bar', items_set.get('foo'))
651- self.assertEqual('eggs', items_set.get('spam'))
652-
653- def test_relation_ids(self):
654- # relation_ids returns a list of relations id for the given
655- # named relation
656- mock_relation_ids = {'test': 'test:1 test:2'}
657- self._patch_command(lambda *args: mock_relation_ids[args[0]])
658- self.assertEqual(mock_relation_ids['test'].split(),
659- charmhelpers.relation_ids('test'))
660-
661- def test_relation_list(self):
662- # relation_list returns a list of unit names either for the current
663- # context or for the provided relation ID
664- mock_unit_names = {'test:1': 'test/0 test/1 test/2',
665- 'test:2': 'test/3 test/4 test/5'}
666-
667- # Patch command for current context use base - context = test:1
668- self._patch_command(lambda: mock_unit_names['test:1'])
669- self.assertEqual(mock_unit_names['test:1'].split(),
670- charmhelpers.relation_list())
671- # Patch command for provided relation-id
672- self._patch_command(lambda *args: mock_unit_names[args[1]])
673- self.assertEqual(mock_unit_names['test:2'].split(),
674- charmhelpers.relation_list(rid='test:2'))
675-
676- def test_open_close_port(self):
677- # expose calls open-port with port/protocol parameters
678- ports_set = []
679-
680- def mock_open_port(*args):
681- for arg in args:
682- ports_set.append(arg)
683-
684- def mock_close_port(*args):
685- if args[0] in ports_set:
686- ports_set.remove(args[0])
687- # Monkey patch in the open-port mock
688- self._patch_command(mock_open_port)
689- charmhelpers.open_port(80, "TCP")
690- charmhelpers.open_port(90, "UDP")
691- charmhelpers.open_port(100)
692- self.assertTrue("80/TCP" in ports_set)
693- self.assertTrue("90/UDP" in ports_set)
694- self.assertTrue("100/TCP" in ports_set)
695- # Monkey patch in the close-port mock function
696- self._patch_command(mock_close_port)
697- charmhelpers.close_port(80, "TCP")
698- charmhelpers.close_port(90, "UDP")
699- charmhelpers.close_port(100)
700- # ports_set should now be empty
701- self.assertEquals(len(ports_set), 0)
702-
703- def test_service_control(self):
704- # Collect commands that have been run
705- commands_set = {}
706-
707- def mock_service(*args):
708- service = args[0]
709- action = args[1]
710- if service not in commands_set:
711- commands_set[service] = []
712- if ((len(commands_set[service]) > 1 and
713- commands_set[service][-1] == 'stop' and
714- action == 'restart')):
715- # Service is stopped - so needs 'start'
716- # action as restart will fail
717- commands_set[service].append(action)
718- raise CalledProcessError(1, repr(args))
719- else:
720- commands_set[service].append(action)
721-
722- result = ['start', 'stop', 'restart', 'start']
723-
724- # Monkey patch service command
725- self._patch_command(mock_service)
726- charmhelpers.service_control('myservice', 'start')
727- charmhelpers.service_control('myservice', 'stop')
728- charmhelpers.service_control('myservice', 'restart')
729- self.assertEquals(result, commands_set['myservice'])
730
731 def test_make_charm_config_file(self):
732 # make_charm_config_file() writes the passed configuration to a
733@@ -428,48 +286,6 @@
734 RuntimeError, charmhelpers.wait_for_page_contents,
735 'http://example.com', "This will error", timeout=0)
736
737- def test_log(self):
738- # The "log" function forwards a string on to the juju-log command.
739- logged = []
740-
741- def juju_log(*args):
742- logged.append(args)
743- charmhelpers.log('This is a log message', juju_log)
744- # Since we only logged one message, juju-log was only called once..
745- self.assertEqual(len(logged), 1)
746- # The message was included in the arguments passed to juju-log.
747- self.assertIn('This is a log message', logged[0])
748-
749- def test_log_escapes_message(self):
750- # The Go version of juju-log interprets any string begining with two
751- # hyphens ("--") as a command-line switch, even if the third character
752- # is non-alphanumeric. This is different behavior than the Python
753- # version of juju-log. Therefore we signfiy the end of options by
754- # inserting the string " -- " just before the log message.
755- logged = []
756-
757- def juju_log(*args):
758- logged.append(args)
759- charmhelpers.log('This is a log message', juju_log)
760- # The call to juju-log includes the " -- " string before the message.
761- self.assertEqual([('--', 'This is a log message')], logged)
762-
763- def test_log_entry(self):
764- # The log_entry function logs a message about the script starting.
765- logged = []
766- self.patch(charmhelpers, 'log', logged.append)
767- self.patch(charmhelpers, 'script_name', lambda: 'SCRIPT-NAME')
768- charmhelpers.log_entry()
769- self.assertEqual(['--> Entering SCRIPT-NAME'], logged)
770-
771- def test_log_exit(self):
772- # The log_exit function logs a message about the script ending.
773- logged = []
774- self.patch(charmhelpers, 'log', logged.append)
775- self.patch(charmhelpers, 'script_name', lambda: 'SCRIPT-NAME')
776- charmhelpers.log_exit()
777- self.assertEqual(['<-- Exiting SCRIPT-NAME'], logged)
778-
779
780 if __name__ == '__main__':
781 unittest.main()
782
783=== modified file 'tests/contrib/jujugui/test_utils.py'
784--- tests/contrib/jujugui/test_utils.py 2013-05-11 20:39:53 +0000
785+++ tests/contrib/jujugui/test_utils.py 2013-05-23 20:50:31 +0000
786@@ -468,17 +468,17 @@
787 class TestCmdLog(unittest.TestCase):
788
789 def setUp(self):
790- # Patch the charmhelpers 'command', which powers get_config. The
791+ # Patch the utils 'config', which powers get_config. The
792 # result of this is the mock_config dictionary will be returned.
793 # The monkey patch is undone in the tearDown.
794- self.command = charmhelpers.command
795+ self.config = utils.config
796 fd, self.log_file_name = tempfile.mkstemp()
797 os.close(fd)
798 mock_config = {'command-log-file': self.log_file_name}
799- charmhelpers.command = lambda *args: lambda: dumps(mock_config)
800+ utils.config = lambda *args: mock_config
801
802 def tearDown(self):
803- charmhelpers.command = self.command
804+ utils.config = self.config
805 os.unlink(self.log_file_name)
806
807 def test_contents_logged(self):
808@@ -491,16 +491,14 @@
809
810 def setUp(self):
811 self.service_names = []
812- self.actions = []
813 self.svc_ctl_call_count = 0
814 self.fake_zk_address = '192.168.5.26'
815 # Monkey patches.
816 self.command = charmhelpers.command
817
818- def service_control_mock(service_name, action):
819+ def service_start_mock(service_name):
820 self.svc_ctl_call_count += 1
821 self.service_names.append(service_name)
822- self.actions.append(action)
823
824 def noop(*args):
825 pass
826@@ -522,7 +520,7 @@
827 self.files[os.path.basename(dest)] = fp.read()
828
829 self.functions = dict(
830- service_control=(utils.service_control, service_control_mock),
831+ service_start=(utils.service_start, service_start_mock),
832 log=(utils.log, noop),
833 su=(utils.su, su),
834 run=(utils.run, noop),
835@@ -555,7 +553,6 @@
836 self.assertTrue(self.ssl_cert_path in conf)
837 self.assertEqual(self.svc_ctl_call_count, 1)
838 self.assertEqual(self.service_names, ['juju-api-improv'])
839- self.assertEqual(self.actions, [charmhelpers.START])
840
841 def test_start_agent(self):
842 start_agent(self.ssl_cert_path, 'config')
843@@ -565,7 +562,6 @@
844 self.assertTrue(self.ssl_cert_path in conf)
845 self.assertEqual(self.svc_ctl_call_count, 1)
846 self.assertEqual(self.service_names, ['juju-api-agent'])
847- self.assertEqual(self.actions, [charmhelpers.START])
848
849 def test_start_gui(self):
850 ssl_cert_path = '/tmp/certificates/'
851
852=== added directory 'tests/core'
853=== added file 'tests/core/__init__.py'
854=== renamed file 'tests/contrib/charmsupport/test_hookenv.py' => 'tests/core/test_hookenv.py'
855--- tests/contrib/charmsupport/test_hookenv.py 2013-05-11 20:24:29 +0000
856+++ tests/core/test_hookenv.py 2013-05-23 20:50:31 +0000
857@@ -1,11 +1,26 @@
858 import json
859
860-from mock import patch, call, MagicMock
861+from mock import patch, call, mock_open
862+from StringIO import StringIO
863+from mock import MagicMock
864 from testtools import TestCase
865 import yaml
866
867-from charmhelpers.contrib.charmsupport import hookenv
868+from charmhelpers.core import hookenv
869
870+CHARM_METADATA = """name: testmock
871+summary: test mock summary
872+description: test mock description
873+requires:
874+ testreqs:
875+ interface: mock
876+provides:
877+ testprov:
878+ interface: mock
879+peers:
880+ testpeer:
881+ interface: mock
882+"""
883
884 class SerializableTest(TestCase):
885 def test_serializes_object_to_json(self):
886@@ -105,14 +120,14 @@
887 check_output.assert_called_with(['config-get', 'baz', '--format=json'])
888
889 @patch('subprocess.check_output')
890- @patch('charmhelpers.contrib.charmsupport.hookenv.log')
891+ @patch('charmhelpers.core.hookenv.log')
892 def test_logs_and_reraises_on_config_error(self, log, check_output):
893 error = 'some error'
894 check_output.side_effect = ValueError(error)
895
896 self.assertRaisesRegexp(ValueError, error, hookenv.config)
897
898- @patch('charmhelpers.contrib.charmsupport.hookenv.os')
899+ @patch('charmhelpers.core.hookenv.os')
900 def test_gets_the_local_unit(self, os_):
901 os_.environ = {
902 'JUJU_UNIT_NAME': 'foo',
903@@ -120,7 +135,7 @@
904
905 self.assertEqual(hookenv.local_unit(), 'foo')
906
907- @patch('charmhelpers.contrib.charmsupport.hookenv.os')
908+ @patch('charmhelpers.core.hookenv.os')
909 def test_checks_that_is_running_in_relation_hook(self, os_):
910 os_.environ = {
911 'JUJU_RELATION': 'foo',
912@@ -128,7 +143,7 @@
913
914 self.assertTrue(hookenv.in_relation_hook())
915
916- @patch('charmhelpers.contrib.charmsupport.hookenv.os')
917+ @patch('charmhelpers.core.hookenv.os')
918 def test_checks_that_is_not_running_in_relation_hook(self, os_):
919 os_.environ = {
920 'bar': 'foo',
921@@ -136,7 +151,7 @@
922
923 self.assertFalse(hookenv.in_relation_hook())
924
925- @patch('charmhelpers.contrib.charmsupport.hookenv.os')
926+ @patch('charmhelpers.core.hookenv.os')
927 def test_gets_the_relation_type(self, os_):
928 os_.environ = {
929 'JUJU_RELATION': 'foo',
930@@ -144,13 +159,13 @@
931
932 self.assertEqual(hookenv.relation_type(), 'foo')
933
934- @patch('charmhelpers.contrib.charmsupport.hookenv.os')
935+ @patch('charmhelpers.core.hookenv.os')
936 def test_relation_type_none_if_not_in_environment(self, os_):
937 os_.environ = {}
938 self.assertEqual(hookenv.relation_type(), None)
939
940 @patch('subprocess.check_output')
941- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_type')
942+ @patch('charmhelpers.core.hookenv.relation_type')
943 def test_gets_relation_ids(self, relation_type, check_output):
944 ids = [1, 2, 3]
945 check_output.return_value = json.dumps(ids)
946@@ -164,7 +179,7 @@
947 reltype])
948
949 @patch('subprocess.check_output')
950- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_type')
951+ @patch('charmhelpers.core.hookenv.relation_type')
952 def test_relation_ids_no_relation_type(self, relation_type, check_output):
953 ids = [1, 2, 3]
954 check_output.return_value = json.dumps(ids)
955@@ -172,11 +187,10 @@
956
957 result = hookenv.relation_ids()
958
959- self.assertEqual(result, ids)
960- check_output.assert_called_with(['relation-ids', '--format=json'])
961+ self.assertEqual(result, [])
962
963 @patch('subprocess.check_output')
964- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_type')
965+ @patch('charmhelpers.core.hookenv.relation_type')
966 def test_gets_relation_ids_for_type(self, relation_type, check_output):
967 ids = [1, 2, 3]
968 check_output.return_value = json.dumps(ids)
969@@ -190,7 +204,7 @@
970 self.assertFalse(relation_type.called)
971
972 @patch('subprocess.check_output')
973- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_id')
974+ @patch('charmhelpers.core.hookenv.relation_id')
975 def test_gets_related_units(self, relation_id, check_output):
976 relid = 123
977 units = ['foo', 'bar']
978@@ -204,7 +218,7 @@
979 '-r', relid])
980
981 @patch('subprocess.check_output')
982- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_id')
983+ @patch('charmhelpers.core.hookenv.relation_id')
984 def test_related_units_no_relation(self, relation_id, check_output):
985 units = ['foo', 'bar']
986 relation_id.return_value = None
987@@ -216,7 +230,7 @@
988 check_output.assert_called_with(['relation-list', '--format=json'])
989
990 @patch('subprocess.check_output')
991- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_id')
992+ @patch('charmhelpers.core.hookenv.relation_id')
993 def test_gets_related_units_for_id(self, relation_id, check_output):
994 relid = 123
995 units = ['foo', 'bar']
996@@ -229,7 +243,7 @@
997 '-r', relid])
998 self.assertFalse(relation_id.called)
999
1000- @patch('charmhelpers.contrib.charmsupport.hookenv.os')
1001+ @patch('charmhelpers.core.hookenv.os')
1002 def test_gets_the_remote_unit(self, os_):
1003 os_.environ = {
1004 'JUJU_REMOTE_UNIT': 'foo',
1005@@ -237,8 +251,8 @@
1006
1007 self.assertEqual(hookenv.remote_unit(), 'foo')
1008
1009- @patch('charmhelpers.contrib.charmsupport.hookenv.remote_unit')
1010- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_get')
1011+ @patch('charmhelpers.core.hookenv.remote_unit')
1012+ @patch('charmhelpers.core.hookenv.relation_get')
1013 def test_gets_relation_for_unit(self, relation_get, remote_unit):
1014 unit = 'foo-unit'
1015 raw_relation = {
1016@@ -254,8 +268,8 @@
1017 self.assertEqual(getattr(result, 'baz-list'), ['1', '2', '3'])
1018 relation_get.assert_called_with(unit=unit, rid=None)
1019
1020- @patch('charmhelpers.contrib.charmsupport.hookenv.remote_unit')
1021- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_get')
1022+ @patch('charmhelpers.core.hookenv.remote_unit')
1023+ @patch('charmhelpers.core.hookenv.relation_get')
1024 def test_gets_relation_for_specific_unit(self, relation_get, remote_unit):
1025 unit = 'foo-unit'
1026 raw_relation = {
1027@@ -271,9 +285,9 @@
1028 relation_get.assert_called_with(unit=unit, rid=None)
1029 self.assertFalse(remote_unit.called)
1030
1031- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_ids')
1032- @patch('charmhelpers.contrib.charmsupport.hookenv.related_units')
1033- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_for_unit')
1034+ @patch('charmhelpers.core.hookenv.relation_ids')
1035+ @patch('charmhelpers.core.hookenv.related_units')
1036+ @patch('charmhelpers.core.hookenv.relation_for_unit')
1037 def test_gets_relations_for_id(self, relation_for_unit, related_units,
1038 relation_ids):
1039 relid = 123
1040@@ -298,9 +312,9 @@
1041 call('bar', relid),
1042 ])
1043
1044- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_ids')
1045- @patch('charmhelpers.contrib.charmsupport.hookenv.related_units')
1046- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_for_unit')
1047+ @patch('charmhelpers.core.hookenv.relation_ids')
1048+ @patch('charmhelpers.core.hookenv.related_units')
1049+ @patch('charmhelpers.core.hookenv.relation_for_unit')
1050 def test_gets_relations_for_specific_id(self, relation_for_unit,
1051 related_units, relation_ids):
1052 relid = 123
1053@@ -325,10 +339,10 @@
1054 ])
1055 self.assertFalse(relation_ids.called)
1056
1057- @patch('charmhelpers.contrib.charmsupport.hookenv.in_relation_hook')
1058- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_type')
1059- @patch('charmhelpers.contrib.charmsupport.hookenv.relation_ids')
1060- @patch('charmhelpers.contrib.charmsupport.hookenv.relations_for_id')
1061+ @patch('charmhelpers.core.hookenv.in_relation_hook')
1062+ @patch('charmhelpers.core.hookenv.relation_type')
1063+ @patch('charmhelpers.core.hookenv.relation_ids')
1064+ @patch('charmhelpers.core.hookenv.relations_for_id')
1065 def test_gets_relations_for_type(self, relations_for_id, relation_ids,
1066 relation_type, in_relation_hook):
1067 reltype = 'foo-type'
1068@@ -366,27 +380,66 @@
1069 call(234),
1070 ])
1071
1072- @patch('charmhelpers.contrib.charmsupport.hookenv.config')
1073- @patch('charmhelpers.contrib.charmsupport.hookenv.local_unit')
1074- @patch('charmhelpers.contrib.charmsupport.hookenv.relations_of_type')
1075- @patch('charmhelpers.contrib.charmsupport.hookenv.os')
1076- def test_gets_execution_environment(self, os_, relations_of_type,
1077- local_unit, config):
1078+ @patch('charmhelpers.core.hookenv.relation_types')
1079+ @patch('charmhelpers.core.hookenv.relation_ids')
1080+ @patch('charmhelpers.core.hookenv.related_units')
1081+ @patch('charmhelpers.core.hookenv.relation_get')
1082+ def test_gets_relations(self, relation_get, related_units,
1083+ relation_ids, relation_types):
1084+ relation_types.return_value = ['t1','t2']
1085+ relation_ids.return_value = ['i1']
1086+ related_units.return_value = ['u1','u2']
1087+ relation_get.return_value = {'key': 'val'}
1088+
1089+ result = hookenv.relations()
1090+
1091+ self.assertEqual(result, {
1092+ 't1': {
1093+ 'i1': {
1094+ 'u1': {'key': 'val'},
1095+ 'u2': {'key': 'val'},
1096+ },
1097+ },
1098+ 't2': {
1099+ 'i1': {
1100+ 'u1': {'key': 'val'},
1101+ 'u2': {'key': 'val'},
1102+ },
1103+ },
1104+ })
1105+
1106+
1107+ @patch('charmhelpers.core.hookenv.config')
1108+ @patch('charmhelpers.core.hookenv.relation_type')
1109+ @patch('charmhelpers.core.hookenv.local_unit')
1110+ @patch('charmhelpers.core.hookenv.relation_id')
1111+ @patch('charmhelpers.core.hookenv.relations')
1112+ @patch('charmhelpers.core.hookenv.relation_get')
1113+ @patch('charmhelpers.core.hookenv.os')
1114+ def test_gets_execution_environment(self, os_, relations_get,
1115+ relations, relation_id, local_unit,
1116+ relation_type, config):
1117 config.return_value = 'some-config'
1118+ relation_type.return_value = 'some-type'
1119 local_unit.return_value = 'some-unit'
1120- relations_of_type.return_value = 'some-relations'
1121+ relation_id.return_value = 'some-id'
1122+ relations.return_value = 'all-relations'
1123+ relations_get.return_value = 'some-relations'
1124 os_.environ = 'some-environment'
1125
1126 result = hookenv.execution_environment()
1127
1128 self.assertEqual(result, {
1129 'conf': 'some-config',
1130+ 'reltype': 'some-type',
1131 'unit': 'some-unit',
1132+ 'relid': 'some-id',
1133 'rel': 'some-relations',
1134+ 'rels': 'all-relations',
1135 'env': 'some-environment',
1136 })
1137
1138- @patch('charmhelpers.contrib.charmsupport.hookenv.os')
1139+ @patch('charmhelpers.core.hookenv.os')
1140 def test_gets_the_relation_id(self, os_):
1141 os_.environ = {
1142 'JUJU_RELATION_ID': 'foo',
1143@@ -394,7 +447,7 @@
1144
1145 self.assertEqual(hookenv.relation_id(), 'foo')
1146
1147- @patch('charmhelpers.contrib.charmsupport.hookenv.os')
1148+ @patch('charmhelpers.core.hookenv.os')
1149 def test_relation_id_none_if_no_env(self, os_):
1150 os_.environ = {}
1151 self.assertEqual(hookenv.relation_id(), None)
1152@@ -407,7 +460,7 @@
1153 result = hookenv.relation_get()
1154
1155 self.assertEqual(result['foo'], 'BAR')
1156- check_output.assert_called_with(['relation-get', '--format=json'])
1157+ check_output.assert_called_with(['relation-get', '--format=json', '-'])
1158
1159 @patch('subprocess.check_output')
1160 def test_gets_relation_with_scope(self, check_output):
1161@@ -443,6 +496,57 @@
1162 check_output.assert_called_with(['relation-get', '--format=json', '-r',
1163 123, 'baz-scope', 'baz-unit'])
1164
1165+ @patch('subprocess.check_call')
1166+ def test_sets_relation(self, check_call_):
1167+ hookenv.relation_set(foo="bar")
1168+ check_call_.assert_called_with(['relation-set','foo=bar'])
1169+
1170+
1171+ @patch('subprocess.check_call')
1172+ def test_sets_relation_with_relation_id(self, check_call_):
1173+ hookenv.relation_set(relation_id="foo", bar="baz")
1174+ check_call_.assert_called_with(['relation-set', '-r', 'foo',
1175+ 'bar=baz'])
1176+
1177+
1178+ def test_lists_relation_types(self):
1179+ open_ = mock_open()
1180+ open_.return_value = StringIO(CHARM_METADATA)
1181+ with patch('charmhelpers.core.hookenv.open', open_, create=True):
1182+ with patch.dict('os.environ', {'CHARM_DIR': '/var/empty'}):
1183+ reltypes = set(hookenv.relation_types())
1184+ open_.assert_called_once_with('/var/empty/metadata.yaml')
1185+ self.assertEqual(set(('testreqs','testprov','testpeer')), reltypes)
1186+
1187+
1188+ @patch('subprocess.check_call')
1189+ def test_opens_port(self, check_call_):
1190+ hookenv.open_port(443, "TCP")
1191+ hookenv.open_port(80)
1192+ hookenv.open_port(100, "UDP")
1193+ calls = [call(['open-port','443/TCP']),
1194+ call(['open-port','80/TCP']),
1195+ call(['open-port','100/UDP']),
1196+ ]
1197+ check_call_.assert_has_calls(calls)
1198+
1199+
1200+ @patch('subprocess.check_call')
1201+ def test_closes_port(self, check_call_):
1202+ hookenv.close_port(443, "TCP")
1203+ hookenv.close_port(80)
1204+ hookenv.close_port(100, "UDP")
1205+ calls = [call(['close-port','443/TCP']),
1206+ call(['close-port','80/TCP']),
1207+ call(['close-port','100/UDP']),
1208+ ]
1209+ check_call_.assert_has_calls(calls)
1210+
1211+ @patch('subprocess.check_output')
1212+ def test_gets_unit_attribute(self, check_output_):
1213+ hookenv.unit_get('foo')
1214+ check_output_.assert_called_with(['unit-get', 'foo'])
1215+
1216
1217 class HooksTest(TestCase):
1218 def test_runs_a_registered_function(self):
1219@@ -488,3 +592,4 @@
1220 self.assertRaises(hookenv.UnregisteredHookError, hooks.execute,
1221 ['brew'])
1222 self.assertEqual(execs, [True])
1223+
1224
1225=== renamed file 'tests/contrib/charmsupport/test_host.py' => 'tests/core/test_host.py'
1226--- tests/contrib/charmsupport/test_host.py 2013-05-11 20:24:29 +0000
1227+++ tests/core/test_host.py 2013-05-23 20:50:31 +0000
1228@@ -5,7 +5,7 @@
1229 from mock import patch, call, MagicMock
1230 from testtools import TestCase
1231
1232-from charmhelpers.contrib.charmsupport import host
1233+from charmhelpers.core import host
1234
1235
1236 MOUNT_LINES = ("""
1237@@ -52,7 +52,7 @@
1238 self.assertTrue(result)
1239 mock_call.assert_called_with(['initctl', action, service_name])
1240 exists.assert_has_calls([
1241- call(os.path.join('/etc/init', service_name)),
1242+ call(os.path.join('/etc/init', '%s.conf' % service_name)),
1243 ])
1244
1245 @patch('subprocess.call')
1246@@ -71,7 +71,6 @@
1247 mock_call.assert_called_with([os.path.join('/etc/init.d',
1248 service_name), action])
1249 exists.assert_has_calls([
1250- call(os.path.join('/etc/init', service_name)),
1251 call(os.path.join('/etc/init.d', service_name)),
1252 ])
1253
1254
1255=== added directory 'tests/payload'
1256=== added file 'tests/payload/__init__.py'
1257=== renamed file 'tests/contrib/charmsupport/test_execd.py' => 'tests/payload/test_execd.py'
1258--- tests/contrib/charmsupport/test_execd.py 2013-05-11 20:24:29 +0000
1259+++ tests/payload/test_execd.py 2013-05-23 20:50:31 +0000
1260@@ -2,10 +2,11 @@
1261 from mock import patch, call
1262 import os
1263 import shutil
1264+from subprocess import CalledProcessError
1265
1266 from tempfile import mkdtemp
1267
1268-from charmhelpers.contrib.charmsupport import execd
1269+from charmhelpers.payload import execd
1270
1271 class ExecDBaseTestCase(TestCase):
1272 def setUp(self):
1273@@ -30,7 +31,7 @@
1274
1275 self.assertEqual(expected, default_dir)
1276
1277- @patch('charmhelpers.contrib.charmsupport.execd.execd_run')
1278+ @patch('charmhelpers.payload.execd.execd_run')
1279 def test_execd_preinstall_calls_charm_pre_install(self, mock_execd_run):
1280 execd_dir = 'testdir'
1281 execd.execd_preinstall(execd_dir)
1282@@ -38,7 +39,7 @@
1283 mock_execd_run.assert_called_with(execd_dir, 'charm-pre-install')
1284
1285
1286- @patch('charmhelpers.contrib.charmsupport.execd.default_execd_dir', return_value='foo')
1287+ @patch('charmhelpers.payload.execd.default_execd_dir', return_value='foo')
1288 @patch('os.listdir', return_value=['a','b','c'])
1289 @patch('os.path.isdir', return_value=True)
1290 def test_execd_module_list_from_env(self, mock_isdir, mock_listdir,
1291@@ -46,7 +47,7 @@
1292 module_names = ['a','b','c']
1293 mock_listdir.return_value = module_names
1294
1295- modules = list(execd.execd_modules())
1296+ modules = list(execd.execd_module_paths())
1297
1298 expected = [os.path.join('foo', d) for d in module_names]
1299 self.assertEqual(modules, expected)
1300@@ -55,7 +56,7 @@
1301 mock_isdir.assert_has_calls([call(d) for d in expected])
1302
1303
1304- @patch('charmhelpers.contrib.charmsupport.execd.default_execd_dir')
1305+ @patch('charmhelpers.payload.execd.default_execd_dir')
1306 @patch('os.listdir', return_value=['a','b','c'])
1307 @patch('os.path.isdir', return_value=True)
1308 def test_execd_module_list_with_dir(self, mock_isdir, mock_listdir,
1309@@ -63,7 +64,7 @@
1310 module_names = ['a','b','c']
1311 mock_listdir.return_value = module_names
1312
1313- modules = list(execd.execd_modules('foo'))
1314+ modules = list(execd.execd_module_paths('foo'))
1315
1316 expected = [os.path.join('foo', d) for d in module_names]
1317 self.assertEqual(modules, expected)
1318@@ -73,16 +74,52 @@
1319 mock_isdir.assert_has_calls([call(d) for d in expected])
1320
1321
1322- @patch('subprocess.check_call')
1323- @patch('charmhelpers.contrib.charmsupport.hookenv.log')
1324 @patch('os.path.isfile', return_value=True)
1325 @patch('os.access', return_value=True)
1326- @patch('charmhelpers.contrib.charmsupport.execd.execd_modules', return_value=['a','b'])
1327- def test_execd_run(self, mock_modules, mock_access, mock_isfile,
1328- mock_log, mock_call):
1329- submodule = 'charm-foo'
1330-
1331- execd.execd_run(submodule)
1332-
1333- paths = [os.path.join(m, submodule) for m in ('a','b')]
1334- mock_call.assert_has_calls([call(d, shell=True) for d in paths])
1335+ @patch('charmhelpers.payload.execd.execd_module_paths')
1336+ def test_execd_submodule_list(self, modlist_, access_, isfile_):
1337+ module_list = ['a','b','c']
1338+ modlist_.return_value = module_list
1339+ submodules = [s for s in execd.execd_submodule_paths('sm')]
1340+
1341+ expected = [os.path.join(d, 'sm') for d in module_list]
1342+ self.assertEqual(submodules, expected)
1343+
1344+
1345+ @patch('subprocess.check_call')
1346+ @patch('charmhelpers.payload.execd.execd_submodule_paths')
1347+ def test_execd_run(self, submods_, call_):
1348+ submod_list = ['a','b','c']
1349+ submods_.return_value = submod_list
1350+ execd.execd_run('foo')
1351+
1352+ submods_.assert_called_with('foo', None)
1353+ call_.assert_has_calls([call(d, shell=True) for d in submod_list])
1354+
1355+
1356+ @patch('subprocess.check_call')
1357+ @patch('charmhelpers.payload.execd.execd_submodule_paths')
1358+ @patch('charmhelpers.core.hookenv.log')
1359+ def test_execd_run_logs_exception(self, log_, submods_, check_call_):
1360+ submod_list = ['a','b','c']
1361+ submods_.return_value = submod_list
1362+ err_msg = 'darn'
1363+ check_call_.side_effect = CalledProcessError(1, 'cmd', err_msg)
1364+
1365+ execd.execd_run('foo')
1366+ log_.assert_called_with(err_msg)
1367+
1368+
1369+ @patch('subprocess.check_call')
1370+ @patch('charmhelpers.payload.execd.execd_submodule_paths')
1371+ @patch('charmhelpers.core.hookenv.log')
1372+ @patch('sys.exit')
1373+ def test_execd_run_dies_with_return_code(self, exit_, log_, submods_,
1374+ check_call_):
1375+ submod_list = ['a','b','c']
1376+ submods_.return_value = submod_list
1377+ retcode = 9
1378+ check_call_.side_effect = CalledProcessError(retcode, 'cmd')
1379+
1380+ execd.execd_run('foo', die_on_error=True)
1381+ exit_.assert_called_with(retcode)

Subscribers

People subscribed via source and target branches