Merge lp:~jason-hobbs/charms/trusty/odl-controller/add-unit-tests into lp:~sdn-charmers/charms/trusty/odl-controller/trunk

Proposed by Jason Hobbs
Status: Needs review
Proposed branch: lp:~jason-hobbs/charms/trusty/odl-controller/add-unit-tests
Merge into: lp:~sdn-charmers/charms/trusty/odl-controller/trunk
Diff against target: 324 lines (+275/-18)
4 files modified
hooks/odl_controller_utils.py (+26/-18)
unit_tests/__init__.py (+3/-0)
unit_tests/test_odl_controller_utils.py (+125/-0)
unit_tests/test_utils.py (+121/-0)
To merge this branch: bzr merge lp:~jason-hobbs/charms/trusty/odl-controller/add-unit-tests
Reviewer Review Type Date Requested Status
SDN Charmers Pending
Review via email: mp+263431@code.launchpad.net

Commit message

Add unit tests for the maven settings template generator.

Description of the change

Just a start on unit tests for odl controller, along with a minor refactor on one of the methods being tested. I copied test_utils.py from the nova-compute charm.

To post a comment you must log in.

Unmerged revisions

9. By Jason Hobbs

Add some docstrings.

8. By Jason Hobbs

One more unit test for no_proxy.

7. By Jason Hobbs

Finish adding unit tests for odl_controller_utils.

6. By Jason Hobbs

Continue adding unit tests.

5. By Jason Hobbs

Add first unit test for ODL utils.

4. By Jason Hobbs

Use early return from mvn_proxy_ctx().

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/odl_controller_utils.py'
2--- hooks/odl_controller_utils.py 2015-02-19 22:08:13 +0000
3+++ hooks/odl_controller_utils.py 2015-07-02 21:50:28 +0000
4@@ -9,29 +9,37 @@
5 ctx.update(mvn_proxy_ctx("https"))
6 return ctx
7
8+
9 def mvn_proxy_ctx(protocol):
10 ctx = {}
11 key = protocol + "_proxy"
12- if key in environ:
13- url = urlparse.urlparse(environ[key])
14- hostname = url.hostname
15- if hostname:
16- ctx[key] = True
17- ctx[protocol + "_proxy_host"] = hostname
18- port = url.port
19- ctx[protocol + "_proxy_port"] = port if port else 80
20- username = url.username
21- if username:
22- ctx[protocol + "_proxy_username"] = username
23- ctx[protocol + "_proxy_password"] = url.password
24- no_proxy = []
25- if "no_proxy" in environ:
26- np = environ["no_proxy"]
27- if np:
28- no_proxy = np.split(",")
29- ctx[protocol + "_noproxy"] = no_proxy
30+ if key not in environ:
31+ return ctx
32+
33+ url = urlparse.urlparse(environ[key])
34+ hostname = url.hostname
35+
36+ if not hostname:
37+ return ctx
38+
39+ ctx[key] = True
40+ ctx[protocol + "_proxy_host"] = hostname
41+ port = url.port
42+ ctx[protocol + "_proxy_port"] = port if port else 80
43+ username = url.username
44+ if username:
45+ ctx[protocol + "_proxy_username"] = username
46+ ctx[protocol + "_proxy_password"] = url.password
47+ no_proxy = []
48+ if "no_proxy" in environ:
49+ np = environ["no_proxy"]
50+ if np:
51+ no_proxy = np.split(",")
52+ ctx[protocol + "_noproxy"] = no_proxy
53+
54 return ctx
55
56+
57 def write_mvn_config():
58 ctx = mvn_ctx()
59 render("settings.xml", "/home/opendaylight/.m2/settings.xml", ctx,
60
61=== added directory 'unit_tests'
62=== added file 'unit_tests/__init__.py'
63--- unit_tests/__init__.py 1970-01-01 00:00:00 +0000
64+++ unit_tests/__init__.py 2015-07-02 21:50:28 +0000
65@@ -0,0 +1,3 @@
66+import sys
67+
68+sys.path.append('hooks/')
69
70=== added file 'unit_tests/test_odl_controller_utils.py'
71--- unit_tests/test_odl_controller_utils.py 1970-01-01 00:00:00 +0000
72+++ unit_tests/test_odl_controller_utils.py 2015-07-02 21:50:28 +0000
73@@ -0,0 +1,125 @@
74+import odl_controller_utils as utils
75+import xml.etree.ElementTree as ET
76+
77+from mock import patch
78+
79+from test_utils import (
80+ CharmTestCase,
81+ patch_open,
82+ )
83+
84+
85+class TestWriteMvnConfig(CharmTestCase):
86+ """Unit tests for utils.write_mvn_config().
87+
88+ These tests all follow the pattern of mocking environ
89+ to manipulate the http_proxy variables write_mvn_config()
90+ uses to determine what variables will be supplied when
91+ rendering the settings.xml template.
92+ """
93+
94+ def setUp(self):
95+ super(TestMvnProxyCtx, self).setUp(utils, [])
96+
97+ @patch('charmhelpers.core.host.mkdir')
98+ @patch('charmhelpers.core.host.write_file')
99+ @patch('charmhelpers.core.hookenv.charm_dir')
100+ def get_rendered_settings(self, charm_dir, write_file, mkdir):
101+ """Call write_mvn_config() to generate XML settings.
102+
103+ Uses mocks in the right places to prevent interaction with the
104+ filesystem and to intercept the write to settings.xml.
105+
106+ This returns the root element of the XML as a lxml.etree.Element.
107+ """
108+ charm_dir.return_value = '.'
109+ result = utils.write_mvn_config()
110+ first_call = write_file.call_args[0]
111+ path = first_call[0]
112+ self.assertEqual(
113+ '/home/opendaylight/.m2/settings.xml',
114+ path)
115+ contents = first_call[1]
116+ root = ET.fromstring(contents)
117+ return root
118+
119+ @patch.dict(utils.environ, {})
120+ def test_proxies_empty_if_proxy_not_provided(self):
121+ root = self.get_rendered_settings()
122+ self.assertEqual([], root.findall('./proxies//*'))
123+
124+ @patch.dict(utils.environ, {'http_proxy': '-'})
125+ def test_proxies_empty_if_hostname_not_in_url(self):
126+ root = self.get_rendered_settings()
127+ self.assertEqual([], root.findall('./proxies//*'))
128+
129+ def assertExpectedProxySettings(self, proxy, protocol, overrides=None):
130+ """Assert that a 'proxy' element contains the expected settings.
131+
132+ protocol -- settings vary based on protocol, which can be either 'http'
133+ or 'https'.
134+
135+ overrides -- a dictionary providing additional settings to verify,
136+ or overriding the default settings (see below for default settings).
137+ """
138+ self.assertIsNotNone(proxy)
139+
140+ expected_values = {
141+ 'id': "%s_proxy" % protocol,
142+ 'active': 'true',
143+ 'protocol': protocol,
144+ 'host': 'url.com',
145+ 'port': '80',
146+ 'nonProxyHosts': None,
147+ }
148+
149+ if overrides is not None:
150+ for key, value in overrides.iteritems():
151+ expected_values[key] = value
152+
153+ for key, value in expected_values.iteritems():
154+ element = proxy.find(key)
155+ self.assertIsNotNone(element)
156+ self.assertEqual(value, element.text)
157+
158+ @patch.dict(utils.environ, {'http_proxy': 'http://url.com/'})
159+ def test_http_proxy_specified(self):
160+ root = self.get_rendered_settings()
161+ proxy = root.find('.//proxies/proxy')
162+ self.assertExpectedProxySettings(proxy, 'http')
163+
164+ @patch.dict(utils.environ, {'https_proxy': 'http://url.com/'})
165+ def test_https_proxy_specified(self):
166+ root = self.get_rendered_settings()
167+ proxy = root.find('.//proxies/proxy')
168+ self.assertExpectedProxySettings(proxy, 'https')
169+
170+ @patch.dict(utils.environ,
171+ {'http_proxy': 'http://url.com/', 'https_proxy': 'http://url.com/'})
172+ def test_http_and_https_proxy_specified(self):
173+ root = self.get_rendered_settings()
174+ http_proxy, https_proxy = root.findall('.//proxies/proxy')
175+ self.assertExpectedProxySettings(http_proxy, 'http')
176+ self.assertExpectedProxySettings(https_proxy, 'https')
177+
178+ @patch.dict(utils.environ, {'http_proxy': 'http://url.com:8491/'})
179+ def test_uses_provided_port(self):
180+ root = self.get_rendered_settings()
181+ proxy = root.find('.//proxies/proxy')
182+ self.assertExpectedProxySettings(proxy, 'http', {'port': '8491'})
183+
184+ @patch.dict(utils.environ, {'http_proxy': 'http://u:p@url.com/'})
185+ def test_uses_username_and_password(self):
186+ root = self.get_rendered_settings()
187+ proxy = root.find('.//proxies/proxy')
188+ overrides = {'username': 'u', 'password': 'p'}
189+ self.assertExpectedProxySettings(proxy, 'http', overrides)
190+
191+ @patch.dict(
192+ utils.environ,
193+ {'http_proxy': 'http://url.com/', 'no_proxy': 'host1,host2'})
194+ def test_uses_no_proxy(self):
195+ root = self.get_rendered_settings()
196+ proxy = root.find('.//proxies/proxy')
197+ self.assertExpectedProxySettings(
198+ proxy, 'http', {'nonProxyHosts': 'host1|host2'})
199
200=== added file 'unit_tests/test_utils.py'
201--- unit_tests/test_utils.py 1970-01-01 00:00:00 +0000
202+++ unit_tests/test_utils.py 2015-07-02 21:50:28 +0000
203@@ -0,0 +1,121 @@
204+import logging
205+import unittest
206+import os
207+import yaml
208+
209+from contextlib import contextmanager
210+from mock import patch, MagicMock
211+
212+
213+def load_config():
214+ '''
215+ Walk backwords from __file__ looking for config.yaml, load and return the
216+ 'options' section'
217+ '''
218+ config = None
219+ f = __file__
220+ while config is None:
221+ d = os.path.dirname(f)
222+ if os.path.isfile(os.path.join(d, 'config.yaml')):
223+ config = os.path.join(d, 'config.yaml')
224+ break
225+ f = d
226+
227+ if not config:
228+ logging.error('Could not find config.yaml in any parent directory '
229+ 'of %s. ' % file)
230+ raise Exception
231+
232+ return yaml.safe_load(open(config).read())['options']
233+
234+
235+def get_default_config():
236+ '''
237+ Load default charm config from config.yaml return as a dict.
238+ If no default is set in config.yaml, its value is None.
239+ '''
240+ default_config = {}
241+ config = load_config()
242+ for k, v in config.iteritems():
243+ if 'default' in v:
244+ default_config[k] = v['default']
245+ else:
246+ default_config[k] = None
247+ return default_config
248+
249+
250+class CharmTestCase(unittest.TestCase):
251+
252+ def setUp(self, obj, patches):
253+ super(CharmTestCase, self).setUp()
254+ self.patches = patches
255+ self.obj = obj
256+ self.test_config = TestConfig()
257+ self.test_relation = TestRelation()
258+ self.patch_all()
259+
260+ def patch(self, method):
261+ _m = patch.object(self.obj, method)
262+ mock = _m.start()
263+ self.addCleanup(_m.stop)
264+ return mock
265+
266+ def patch_all(self):
267+ for method in self.patches:
268+ setattr(self, method, self.patch(method))
269+
270+
271+class TestConfig(object):
272+
273+ def __init__(self):
274+ self.config = get_default_config()
275+
276+ def get(self, attr=None):
277+ if not attr:
278+ return self.get_all()
279+ try:
280+ return self.config[attr]
281+ except KeyError:
282+ return None
283+
284+ def get_all(self):
285+ return self.config
286+
287+ def set(self, attr, value):
288+ if attr not in self.config:
289+ raise KeyError
290+ self.config[attr] = value
291+
292+
293+class TestRelation(object):
294+
295+ def __init__(self, relation_data={}):
296+ self.relation_data = relation_data
297+
298+ def set(self, relation_data):
299+ self.relation_data = relation_data
300+
301+ def get(self, attr=None, unit=None, rid=None):
302+ if attr is None:
303+ return self.relation_data
304+ elif attr in self.relation_data:
305+ return self.relation_data[attr]
306+ return None
307+
308+
309+@contextmanager
310+def patch_open():
311+ '''Patch open() to allow mocking both open() itself and the file that is
312+ yielded.
313+
314+ Yields the mock for "open" and "file", respectively.'''
315+ mock_open = MagicMock(spec=open)
316+ mock_file = MagicMock(spec=file)
317+
318+ @contextmanager
319+ def stub_open(*args, **kwargs):
320+ mock_open(*args, **kwargs)
321+ yield mock_file
322+
323+ with patch('__builtin__.open', stub_open):
324+ yield mock_open, mock_file

Subscribers

People subscribed via source and target branches

to all changes: