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
=== modified file 'assess_heterogeneous_control.py'
--- assess_heterogeneous_control.py 2016-04-09 06:17:50 +0000
+++ assess_heterogeneous_control.py 2016-04-15 17:05:32 +0000
@@ -203,13 +203,16 @@
203 raise AssertionError('Machine not destroyed: {}.'.format(agent_id))203 raise AssertionError('Machine not destroyed: {}.'.format(agent_id))
204204
205205
206def check_series(client):206def check_series(client, machine='0', series=None):
207 """Use 'juju ssh' to check that the deployed series meets expectations."""207 """Use 'juju ssh' to check that the deployed series meets expectations."""
208 result = client.get_juju_output('ssh', '0', 'lsb_release', '-c')208 result = client.get_juju_output('ssh', machine, 'lsb_release', '-c')
209 label, codename = result.rstrip().split('\t')209 label, codename = result.rstrip().split('\t')
210 if label != 'Codename:':210 if label != 'Codename:':
211 raise AssertionError()211 raise AssertionError()
212 expected_codename = client.env.config['default-series']212 if series:
213 expected_codename = series
214 else:
215 expected_codename = client.env.config['default-series']
213 if codename != expected_codename:216 if codename != expected_codename:
214 raise AssertionError(217 raise AssertionError(
215 'Series is {}, not {}'.format(codename, expected_codename))218 'Series is {}, not {}'.format(codename, expected_codename))
216219
=== added file 'assess_multi_series_charms.py'
--- assess_multi_series_charms.py 1970-01-01 00:00:00 +0000
+++ assess_multi_series_charms.py 2016-04-15 17:05:32 +0000
@@ -0,0 +1,133 @@
1#!/usr/bin/env python
2"""
3Charms have the capability to declare that they support more than
4one series. Previously a separate copy of the charm was required for
5each series. An important constraint here is that for a given charm,
6all of the listed series must be for the same distro/OS; it is not
7allowed to offer a single charm for Ubuntu and CentOS for example.
8Supported series are added to charm metadata as follows:
9
10 name: mycharm
11 summary: "Great software"
12 description: It works
13 series:
14 - trusty
15 - precise
16 - wily
17
18The default series is the first in the list:
19
20 juju deploy mycharm
21
22should deploy a mycharm service running on trusty.
23
24A different, non-default series may be specified:
25
26 juju deploy mycharm --series precise
27
28It is possible to force the charm to deploy using an unsupported series
29(so long as the underlying OS is compatible):
30
31 juju deploy mycharm --series xenial --force
32
33"""
34from __future__ import print_function
35
36import argparse
37from collections import namedtuple
38import logging
39import subprocess
40import sys
41
42from deploy_stack import BootstrapManager
43from utility import (
44 add_basic_testing_arguments,
45 configure_logging,
46 make_charm,
47 temp_dir,
48)
49from assess_heterogeneous_control import check_series
50from assess_min_version import JujuAssertionError
51
52
53__metaclass__ = type
54
55log = logging.getLogger("assess_multi_series_charms")
56
57
58Test = namedtuple("Test", ["series", "service", "force", "success", "machine"])
59
60
61def assess_multi_series_charms(client):
62 """Assess multi series charms.
63
64 :param client: Juju client.
65 :type client: jujupy.EnvJujuClient
66 :return: None
67 """
68 tests = [
69 Test(series="precise", service='test0', force=False, success=False,
70 machine=None),
71 Test(series=None, service='test1', force=False, success=True,
72 machine='0'),
73 Test(series="trusty", service='test2', force=False, success=True,
74 machine='1'),
75 Test(series="xenial", service='test3', force=False, success=True,
76 machine='2'),
77 Test(series="precise", service='test4', force=True, success=True,
78 machine='3'),
79 ]
80 with temp_dir() as charm_dir:
81 make_charm(charm_dir, series=['trusty', 'xenial'])
82 for test in tests:
83 log.info(
84 "Assessing multi series charms: test: {} charm_dir:{}".format(
85 test, charm_dir))
86 assert_deploy(client, test, charm_dir)
87 if test.machine:
88 check_series(client, machine=test.machine, series=test.series)
89
90
91def assert_deploy(client, test, charm_dir):
92 """Deploy a charm and assert a success or fail.
93
94 :param client: Juju client
95 :type client: jujupy.EnvJujuClient
96 :param test: Deploy test data.
97 :type test: Test
98 :param charm_dir:
99 :type charm_dir: str
100 :return: None
101 """
102 if test.success:
103 client.deploy(charm=charm_dir, series=test.series,
104 service=test.service, force=test.force)
105 client.wait_for_started()
106 else:
107 try:
108 client.deploy(charm=charm_dir, series=test.series,
109 service=test.service, force=test.force)
110 except subprocess.CalledProcessError:
111 return
112 raise JujuAssertionError('Assert deploy failed for {}'.format(test))
113
114
115def parse_args(argv):
116 """Parse all arguments."""
117 parser = argparse.ArgumentParser(
118 description="Test multi series charm feature")
119 add_basic_testing_arguments(parser)
120 return parser.parse_args(argv)
121
122
123def main(argv=None):
124 args = parse_args(argv)
125 configure_logging(args.verbose)
126 bs_manager = BootstrapManager.from_args(args)
127 with bs_manager.booted_context(args.upload_tools):
128 assess_multi_series_charms(bs_manager.client)
129 return 0
130
131
132if __name__ == '__main__':
133 sys.exit(main())
0134
=== modified file 'tests/test_assess_heterogeneous_control.py'
--- tests/test_assess_heterogeneous_control.py 2016-04-09 06:17:50 +0000
+++ tests/test_assess_heterogeneous_control.py 2016-04-15 17:05:32 +0000
@@ -9,6 +9,7 @@
99
10from assess_heterogeneous_control import (10from assess_heterogeneous_control import (
11 assess_heterogeneous,11 assess_heterogeneous,
12 check_series,
12 get_clients,13 get_clients,
13 parse_args,14 parse_args,
14 test_control_heterogeneous,15 test_control_heterogeneous,
@@ -149,3 +150,28 @@
149 test_control_heterogeneous(bs_manager, other_client, True)150 test_control_heterogeneous(bs_manager, other_client, True)
150 self.assertEqual(initial_client.env.juju_home,151 self.assertEqual(initial_client.env.juju_home,
151 other_client.env.juju_home)152 other_client.env.juju_home)
153
154
155class TestCheckSeries(TestCase):
156
157 def test_check_series(self):
158 client = FakeJujuClient()
159 check_series(client)
160
161 def test_check_series_xenial(self):
162 client = MagicMock(spec=["get_juju_output"])
163 client.get_juju_output.return_value = "Codename: xenial"
164 check_series(client, 1, 'xenial')
165
166 def test_check_series_calls(self):
167 client = MagicMock(spec=["get_juju_output"])
168 with patch.object(client, 'get_juju_output',
169 return_value="Codename: xenial") as gjo_mock:
170 check_series(client, 2, 'xenial')
171 gjo_mock.assert_called_once_with('ssh', 2, 'lsb_release', '-c')
172
173 def test_check_series_exceptionl(self):
174 client = FakeJujuClient()
175 with self.assertRaisesRegexp(
176 AssertionError, 'Series is angsty, not xenial'):
177 check_series(client, '0', 'xenial')
152178
=== added file 'tests/test_assess_multi_series_charms.py'
--- tests/test_assess_multi_series_charms.py 1970-01-01 00:00:00 +0000
+++ tests/test_assess_multi_series_charms.py 2016-04-15 17:05:32 +0000
@@ -0,0 +1,140 @@
1"""Tests for assess_multi_series_charms module."""
2
3import logging
4from mock import (
5 call,
6 Mock,
7 patch,
8)
9import StringIO
10import subprocess
11
12from assess_min_version import JujuAssertionError
13from assess_multi_series_charms import (
14 assert_deploy,
15 assess_multi_series_charms,
16 parse_args,
17 main,
18 Test,
19)
20from tests import (
21 parse_error,
22 TestCase,
23)
24from utility import temp_dir
25
26
27class TestParseArgs(TestCase):
28 def test_common_args(self):
29 args = parse_args(["an-env", "/bin/juju", "/tmp/logs", "an-env-mod"])
30 self.assertEqual("an-env", args.env)
31 self.assertEqual("/bin/juju", args.juju_bin)
32 self.assertEqual("/tmp/logs", args.logs)
33 self.assertEqual("an-env-mod", args.temp_env_name)
34 self.assertEqual(False, args.debug)
35
36 def test_help(self):
37 fake_stdout = StringIO.StringIO()
38 with parse_error(self) as fake_stderr:
39 with patch("sys.stdout", fake_stdout):
40 parse_args(["--help"])
41 self.assertEqual("", fake_stderr.getvalue())
42 self.assertNotIn("TODO", fake_stdout.getvalue())
43
44
45class TestMain(TestCase):
46 def test_main(self):
47 argv = ["an-env", "/bin/juju", "/tmp/logs", "an-env-mod", "--verbose"]
48 env = object()
49 client = Mock(spec=["is_jes_enabled"])
50 with patch("assess_multi_series_charms.configure_logging",
51 autospec=True) as mock_cl:
52 with patch("assess_multi_series_charms.BootstrapManager."
53 "booted_context",
54 autospec=True) as mock_bc:
55 with patch("jujupy.SimpleEnvironment.from_config",
56 return_value=env) as mock_e:
57 with patch("jujupy.EnvJujuClient.by_version",
58 return_value=client) as mock_c:
59 with patch("assess_multi_series_charms."
60 "assess_multi_series_charms",
61 autospec=True) as mock_assess:
62 main(argv)
63 mock_cl.assert_called_once_with(logging.DEBUG)
64 mock_e.assert_called_once_with("an-env")
65 mock_c.assert_called_once_with(env, "/bin/juju", debug=False)
66 self.assertEqual(mock_bc.call_count, 1)
67 mock_assess.assert_called_once_with(client)
68
69
70class TestAssess(TestCase):
71
72 def test_assess_multi_series_charms(self):
73 mock_client = Mock(
74 spec=["deploy", "get_juju_output", "wait_for_started"])
75 mock_client.get_juju_output.return_value = "Codename: trusty"
76 mock_client.deploy.side_effect = [
77 subprocess.CalledProcessError(None, None),
78 None,
79 None,
80 None,
81 None
82 ]
83 with temp_dir() as charm_dir:
84 with patch('assess_multi_series_charms.temp_dir',
85 autospec=True) as td_mock:
86 td_mock.return_value.__enter__.return_value = charm_dir
87 with patch('assess_multi_series_charms.check_series',
88 autospec=True) as cs_mock:
89 assess_multi_series_charms(mock_client)
90 self.assertEqual(mock_client.wait_for_started.call_count, 4)
91 calls = [
92 call(charm=charm_dir, force=False, series='precise',
93 service='test0'),
94 call(charm=charm_dir, force=False, series=None, service='test1'),
95 call(charm=charm_dir, force=False, series='trusty',
96 service='test2'),
97 call(charm=charm_dir, force=False, series='xenial',
98 service='test3'),
99 call(charm=charm_dir, force=True, series='precise',
100 service='test4')
101 ]
102 self.assertEqual(mock_client.deploy.mock_calls, calls)
103 td_mock.assert_called_once_with()
104 cs_calls = [
105 call(mock_client, machine='0', series=None),
106 call(mock_client, machine='1', series='trusty'),
107 call(mock_client, machine='2', series='xenial'),
108 call(mock_client, machine='3', series='precise')]
109 self.assertEqual(cs_mock.mock_calls, cs_calls)
110
111 def test_assert_deploy(self):
112 test = Test(series='trusty', service='test1', force=False,
113 success=True, machine='0')
114 mock_client = Mock(
115 spec=["deploy", "get_juju_output", "wait_for_started"])
116 assert_deploy(mock_client, test, '/tmp/foo')
117 mock_client.deploy.assert_called_once_with(
118 charm='/tmp/foo', force=False, series='trusty', service='test1')
119
120 def test_assert_deploy_success_false(self):
121 test = Test(series='trusty', service='test1', force=False,
122 success=False, machine='0')
123 mock_client = Mock(
124 spec=["deploy", "get_juju_output", "wait_for_started"])
125 mock_client.deploy.side_effect = subprocess.CalledProcessError(
126 None, None)
127 assert_deploy(mock_client, test, '/tmp/foo')
128 mock_client.deploy.assert_called_once_with(
129 charm='/tmp/foo', force=False, series='trusty', service='test1')
130
131 def test_assert_deploy_success_false_raises_exception(self):
132 test = Test(series='trusty', service='test1', force=False,
133 success=False, machine='0')
134 mock_client = Mock(
135 spec=["deploy", "get_juju_output", "wait_for_started"])
136 with self.assertRaisesRegexp(
137 JujuAssertionError, 'Assert deploy failed for'):
138 assert_deploy(mock_client, test, '/tmp/foo')
139 mock_client.deploy.assert_called_once_with(
140 charm='/tmp/foo', force=False, series='trusty', service='test1')
0141
=== modified file 'utility.py'
--- utility.py 2016-04-13 22:22:47 +0000
+++ utility.py 2016-04-15 17:05:32 +0000
@@ -476,6 +476,7 @@
476 content['min-juju-version'] = min_ver476 content['min-juju-version'] = min_ver
477 content['summary'] = summary477 content['summary'] = summary
478 content['description'] = description478 content['description'] = description
479 content['series'] = [series] if isinstance(series, str) else series479 if series is not None:
480 content['series'] = [series] if isinstance(series, str) else series
480 with open(metadata, 'w') as f:481 with open(metadata, 'w') as f:
481 yaml.safe_dump(content, f, default_flow_style=False)482 yaml.safe_dump(content, f, default_flow_style=False)

Subscribers

People subscribed via source and target branches