Merge lp:~sseman/juju-ci-tools/multi-series-charm-2 into lp:juju-ci-tools

Proposed by Seman
Status: Merged
Merged at revision: 1360
Proposed branch: lp:~sseman/juju-ci-tools/multi-series-charm-2
Merge into: lp:juju-ci-tools
Diff against target: 360 lines (+307/-4)
5 files modified
assess_heterogeneous_control.py (+6/-3)
assess_multi_series_charms.py (+133/-0)
tests/test_assess_heterogeneous_control.py (+26/-0)
tests/test_assess_multi_series_charms.py (+140/-0)
utility.py (+2/-1)
To merge this branch: bzr merge lp:~sseman/juju-ci-tools/multi-series-charm-2
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+291937@code.launchpad.net

Description of the change

This branch adds tests for multi series charm.

Charms have the capability to declare that they support more than
one series. Previously a separate copy of the charm was required for
each series. Supported series are added to charm metadata as follows:

    name: mycharm
    summary: "Great software"
    description: It works
    series:
       - trusty
       - precise
       - wily

The default series is the first in the list:

    juju deploy mycharm

should deploy a mycharm service running on trusty.

A different, non-default series may be specified:

    juju deploy mycharm --series precise

It is possible to force the charm to deploy using an unsupported series
(so long as the underlying OS is compatible):

    juju deploy mycharm --series xenial --force

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

Very nice.

review: Approve
1360. By Seman

Minor reformatting.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'assess_heterogeneous_control.py'
2--- assess_heterogeneous_control.py 2016-04-09 06:17:50 +0000
3+++ assess_heterogeneous_control.py 2016-04-15 17:05:32 +0000
4@@ -203,13 +203,16 @@
5 raise AssertionError('Machine not destroyed: {}.'.format(agent_id))
6
7
8-def check_series(client):
9+def check_series(client, machine='0', series=None):
10 """Use 'juju ssh' to check that the deployed series meets expectations."""
11- result = client.get_juju_output('ssh', '0', 'lsb_release', '-c')
12+ result = client.get_juju_output('ssh', machine, 'lsb_release', '-c')
13 label, codename = result.rstrip().split('\t')
14 if label != 'Codename:':
15 raise AssertionError()
16- expected_codename = client.env.config['default-series']
17+ if series:
18+ expected_codename = series
19+ else:
20+ expected_codename = client.env.config['default-series']
21 if codename != expected_codename:
22 raise AssertionError(
23 'Series is {}, not {}'.format(codename, expected_codename))
24
25=== added file 'assess_multi_series_charms.py'
26--- assess_multi_series_charms.py 1970-01-01 00:00:00 +0000
27+++ assess_multi_series_charms.py 2016-04-15 17:05:32 +0000
28@@ -0,0 +1,133 @@
29+#!/usr/bin/env python
30+"""
31+Charms have the capability to declare that they support more than
32+one series. Previously a separate copy of the charm was required for
33+each series. An important constraint here is that for a given charm,
34+all of the listed series must be for the same distro/OS; it is not
35+allowed to offer a single charm for Ubuntu and CentOS for example.
36+Supported series are added to charm metadata as follows:
37+
38+ name: mycharm
39+ summary: "Great software"
40+ description: It works
41+ series:
42+ - trusty
43+ - precise
44+ - wily
45+
46+The default series is the first in the list:
47+
48+ juju deploy mycharm
49+
50+should deploy a mycharm service running on trusty.
51+
52+A different, non-default series may be specified:
53+
54+ juju deploy mycharm --series precise
55+
56+It is possible to force the charm to deploy using an unsupported series
57+(so long as the underlying OS is compatible):
58+
59+ juju deploy mycharm --series xenial --force
60+
61+"""
62+from __future__ import print_function
63+
64+import argparse
65+from collections import namedtuple
66+import logging
67+import subprocess
68+import sys
69+
70+from deploy_stack import BootstrapManager
71+from utility import (
72+ add_basic_testing_arguments,
73+ configure_logging,
74+ make_charm,
75+ temp_dir,
76+)
77+from assess_heterogeneous_control import check_series
78+from assess_min_version import JujuAssertionError
79+
80+
81+__metaclass__ = type
82+
83+log = logging.getLogger("assess_multi_series_charms")
84+
85+
86+Test = namedtuple("Test", ["series", "service", "force", "success", "machine"])
87+
88+
89+def assess_multi_series_charms(client):
90+ """Assess multi series charms.
91+
92+ :param client: Juju client.
93+ :type client: jujupy.EnvJujuClient
94+ :return: None
95+ """
96+ tests = [
97+ Test(series="precise", service='test0', force=False, success=False,
98+ machine=None),
99+ Test(series=None, service='test1', force=False, success=True,
100+ machine='0'),
101+ Test(series="trusty", service='test2', force=False, success=True,
102+ machine='1'),
103+ Test(series="xenial", service='test3', force=False, success=True,
104+ machine='2'),
105+ Test(series="precise", service='test4', force=True, success=True,
106+ machine='3'),
107+ ]
108+ with temp_dir() as charm_dir:
109+ make_charm(charm_dir, series=['trusty', 'xenial'])
110+ for test in tests:
111+ log.info(
112+ "Assessing multi series charms: test: {} charm_dir:{}".format(
113+ test, charm_dir))
114+ assert_deploy(client, test, charm_dir)
115+ if test.machine:
116+ check_series(client, machine=test.machine, series=test.series)
117+
118+
119+def assert_deploy(client, test, charm_dir):
120+ """Deploy a charm and assert a success or fail.
121+
122+ :param client: Juju client
123+ :type client: jujupy.EnvJujuClient
124+ :param test: Deploy test data.
125+ :type test: Test
126+ :param charm_dir:
127+ :type charm_dir: str
128+ :return: None
129+ """
130+ if test.success:
131+ client.deploy(charm=charm_dir, series=test.series,
132+ service=test.service, force=test.force)
133+ client.wait_for_started()
134+ else:
135+ try:
136+ client.deploy(charm=charm_dir, series=test.series,
137+ service=test.service, force=test.force)
138+ except subprocess.CalledProcessError:
139+ return
140+ raise JujuAssertionError('Assert deploy failed for {}'.format(test))
141+
142+
143+def parse_args(argv):
144+ """Parse all arguments."""
145+ parser = argparse.ArgumentParser(
146+ description="Test multi series charm feature")
147+ add_basic_testing_arguments(parser)
148+ return parser.parse_args(argv)
149+
150+
151+def main(argv=None):
152+ args = parse_args(argv)
153+ configure_logging(args.verbose)
154+ bs_manager = BootstrapManager.from_args(args)
155+ with bs_manager.booted_context(args.upload_tools):
156+ assess_multi_series_charms(bs_manager.client)
157+ return 0
158+
159+
160+if __name__ == '__main__':
161+ sys.exit(main())
162
163=== modified file 'tests/test_assess_heterogeneous_control.py'
164--- tests/test_assess_heterogeneous_control.py 2016-04-09 06:17:50 +0000
165+++ tests/test_assess_heterogeneous_control.py 2016-04-15 17:05:32 +0000
166@@ -9,6 +9,7 @@
167
168 from assess_heterogeneous_control import (
169 assess_heterogeneous,
170+ check_series,
171 get_clients,
172 parse_args,
173 test_control_heterogeneous,
174@@ -149,3 +150,28 @@
175 test_control_heterogeneous(bs_manager, other_client, True)
176 self.assertEqual(initial_client.env.juju_home,
177 other_client.env.juju_home)
178+
179+
180+class TestCheckSeries(TestCase):
181+
182+ def test_check_series(self):
183+ client = FakeJujuClient()
184+ check_series(client)
185+
186+ def test_check_series_xenial(self):
187+ client = MagicMock(spec=["get_juju_output"])
188+ client.get_juju_output.return_value = "Codename: xenial"
189+ check_series(client, 1, 'xenial')
190+
191+ def test_check_series_calls(self):
192+ client = MagicMock(spec=["get_juju_output"])
193+ with patch.object(client, 'get_juju_output',
194+ return_value="Codename: xenial") as gjo_mock:
195+ check_series(client, 2, 'xenial')
196+ gjo_mock.assert_called_once_with('ssh', 2, 'lsb_release', '-c')
197+
198+ def test_check_series_exceptionl(self):
199+ client = FakeJujuClient()
200+ with self.assertRaisesRegexp(
201+ AssertionError, 'Series is angsty, not xenial'):
202+ check_series(client, '0', 'xenial')
203
204=== added file 'tests/test_assess_multi_series_charms.py'
205--- tests/test_assess_multi_series_charms.py 1970-01-01 00:00:00 +0000
206+++ tests/test_assess_multi_series_charms.py 2016-04-15 17:05:32 +0000
207@@ -0,0 +1,140 @@
208+"""Tests for assess_multi_series_charms module."""
209+
210+import logging
211+from mock import (
212+ call,
213+ Mock,
214+ patch,
215+)
216+import StringIO
217+import subprocess
218+
219+from assess_min_version import JujuAssertionError
220+from assess_multi_series_charms import (
221+ assert_deploy,
222+ assess_multi_series_charms,
223+ parse_args,
224+ main,
225+ Test,
226+)
227+from tests import (
228+ parse_error,
229+ TestCase,
230+)
231+from utility import temp_dir
232+
233+
234+class TestParseArgs(TestCase):
235+ def test_common_args(self):
236+ args = parse_args(["an-env", "/bin/juju", "/tmp/logs", "an-env-mod"])
237+ self.assertEqual("an-env", args.env)
238+ self.assertEqual("/bin/juju", args.juju_bin)
239+ self.assertEqual("/tmp/logs", args.logs)
240+ self.assertEqual("an-env-mod", args.temp_env_name)
241+ self.assertEqual(False, args.debug)
242+
243+ def test_help(self):
244+ fake_stdout = StringIO.StringIO()
245+ with parse_error(self) as fake_stderr:
246+ with patch("sys.stdout", fake_stdout):
247+ parse_args(["--help"])
248+ self.assertEqual("", fake_stderr.getvalue())
249+ self.assertNotIn("TODO", fake_stdout.getvalue())
250+
251+
252+class TestMain(TestCase):
253+ def test_main(self):
254+ argv = ["an-env", "/bin/juju", "/tmp/logs", "an-env-mod", "--verbose"]
255+ env = object()
256+ client = Mock(spec=["is_jes_enabled"])
257+ with patch("assess_multi_series_charms.configure_logging",
258+ autospec=True) as mock_cl:
259+ with patch("assess_multi_series_charms.BootstrapManager."
260+ "booted_context",
261+ autospec=True) as mock_bc:
262+ with patch("jujupy.SimpleEnvironment.from_config",
263+ return_value=env) as mock_e:
264+ with patch("jujupy.EnvJujuClient.by_version",
265+ return_value=client) as mock_c:
266+ with patch("assess_multi_series_charms."
267+ "assess_multi_series_charms",
268+ autospec=True) as mock_assess:
269+ main(argv)
270+ mock_cl.assert_called_once_with(logging.DEBUG)
271+ mock_e.assert_called_once_with("an-env")
272+ mock_c.assert_called_once_with(env, "/bin/juju", debug=False)
273+ self.assertEqual(mock_bc.call_count, 1)
274+ mock_assess.assert_called_once_with(client)
275+
276+
277+class TestAssess(TestCase):
278+
279+ def test_assess_multi_series_charms(self):
280+ mock_client = Mock(
281+ spec=["deploy", "get_juju_output", "wait_for_started"])
282+ mock_client.get_juju_output.return_value = "Codename: trusty"
283+ mock_client.deploy.side_effect = [
284+ subprocess.CalledProcessError(None, None),
285+ None,
286+ None,
287+ None,
288+ None
289+ ]
290+ with temp_dir() as charm_dir:
291+ with patch('assess_multi_series_charms.temp_dir',
292+ autospec=True) as td_mock:
293+ td_mock.return_value.__enter__.return_value = charm_dir
294+ with patch('assess_multi_series_charms.check_series',
295+ autospec=True) as cs_mock:
296+ assess_multi_series_charms(mock_client)
297+ self.assertEqual(mock_client.wait_for_started.call_count, 4)
298+ calls = [
299+ call(charm=charm_dir, force=False, series='precise',
300+ service='test0'),
301+ call(charm=charm_dir, force=False, series=None, service='test1'),
302+ call(charm=charm_dir, force=False, series='trusty',
303+ service='test2'),
304+ call(charm=charm_dir, force=False, series='xenial',
305+ service='test3'),
306+ call(charm=charm_dir, force=True, series='precise',
307+ service='test4')
308+ ]
309+ self.assertEqual(mock_client.deploy.mock_calls, calls)
310+ td_mock.assert_called_once_with()
311+ cs_calls = [
312+ call(mock_client, machine='0', series=None),
313+ call(mock_client, machine='1', series='trusty'),
314+ call(mock_client, machine='2', series='xenial'),
315+ call(mock_client, machine='3', series='precise')]
316+ self.assertEqual(cs_mock.mock_calls, cs_calls)
317+
318+ def test_assert_deploy(self):
319+ test = Test(series='trusty', service='test1', force=False,
320+ success=True, machine='0')
321+ mock_client = Mock(
322+ spec=["deploy", "get_juju_output", "wait_for_started"])
323+ assert_deploy(mock_client, test, '/tmp/foo')
324+ mock_client.deploy.assert_called_once_with(
325+ charm='/tmp/foo', force=False, series='trusty', service='test1')
326+
327+ def test_assert_deploy_success_false(self):
328+ test = Test(series='trusty', service='test1', force=False,
329+ success=False, machine='0')
330+ mock_client = Mock(
331+ spec=["deploy", "get_juju_output", "wait_for_started"])
332+ mock_client.deploy.side_effect = subprocess.CalledProcessError(
333+ None, None)
334+ assert_deploy(mock_client, test, '/tmp/foo')
335+ mock_client.deploy.assert_called_once_with(
336+ charm='/tmp/foo', force=False, series='trusty', service='test1')
337+
338+ def test_assert_deploy_success_false_raises_exception(self):
339+ test = Test(series='trusty', service='test1', force=False,
340+ success=False, machine='0')
341+ mock_client = Mock(
342+ spec=["deploy", "get_juju_output", "wait_for_started"])
343+ with self.assertRaisesRegexp(
344+ JujuAssertionError, 'Assert deploy failed for'):
345+ assert_deploy(mock_client, test, '/tmp/foo')
346+ mock_client.deploy.assert_called_once_with(
347+ charm='/tmp/foo', force=False, series='trusty', service='test1')
348
349=== modified file 'utility.py'
350--- utility.py 2016-04-13 22:22:47 +0000
351+++ utility.py 2016-04-15 17:05:32 +0000
352@@ -476,6 +476,7 @@
353 content['min-juju-version'] = min_ver
354 content['summary'] = summary
355 content['description'] = description
356- content['series'] = [series] if isinstance(series, str) else series
357+ if series is not None:
358+ content['series'] = [series] if isinstance(series, str) else series
359 with open(metadata, 'w') as f:
360 yaml.safe_dump(content, f, default_flow_style=False)

Subscribers

People subscribed via source and target branches