Merge lp:~hduran-8/juju-ci-tools/add_status_ci_tests into lp:juju-ci-tools

Proposed by Horacio Durán
Status: Merged
Merged at revision: 1024
Proposed branch: lp:~hduran-8/juju-ci-tools/add_status_ci_tests
Merge into: lp:juju-ci-tools
Prerequisite: lp:~hduran-8/juju-ci-tools/repository
Diff against target: 1142 lines (+1084/-5)
6 files modified
assess_status_output.py (+154/-0)
base_asses.py (+58/-0)
jujupy.py (+7/-5)
status.py (+385/-0)
template_assess.py.tmpl (+54/-0)
test_assess_status_output.py (+426/-0)
To merge this branch: bzr merge lp:~hduran-8/juju-ci-tools/add_status_ci_tests
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+262150@code.launchpad.net

Description of the change

Added Status CI tests.

CI tests where added to ensure that the format of various
formats of status output remain the same through changes.
The formats covered are Tabular, yaml and json and the
testing is done by getting the same status in all formats
and ensuring all expected data is there and that there is
no unexpected data not covered by testing.

To post a comment you must log in.
981. By Horacio Durán

Merge with master and resolve conflict in jujupy.py

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you Horatio. This was a long branch to review. I like the actual testing done in this branch, but I believe there are easier ways to do it. My comments are inline. I also like the idea of a template to write new tests, and I think there is a context manager that makes writing test easier than what you choose.

Please run make lint to address style issues. you may want to try autopep8 which claims to reformat python to pep8 and pep257. In my experience, there will be some reformatting that is not specified by pep8

review: Needs Fixing (code)
982. By Horacio Durán

Proper python formatting.

983. By Horacio Durán

Fixed a couple of bugs on parse args.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Horatio. Sorry for taking so long to review this branch. We normally keep branches to 400 lines.

I think there is too much duplication with other feature in juju-ci-tools and Python itself. My comments are inline, but in general, there is already a helper to bring up and tear down an env, you should use it because CI needs more control over what is under test then your main() provides. We do not want to write assertions when Python provides them for free. I think all the calls to assert can be written in fewer lines using unittest.
     https://docs.python.org/2/library/unittest.html#unittest.FunctionTestCase

review: Needs Fixing (code)
984. By Horacio Durán

Addressed curtis observations.

I refactored most of my tests to suit CI team
style and requirements as per Curtis comments.

Revision history for this message
Horacio Durán (hduran-8) wrote :

Hey the only thing I dont agree with is the issue about the literals for status output, I added a comment there, I dont really think we should use equivalent since the literal status output is what we are testing, I added more detail in the comment.

985. By Horacio Durán

Addressed extra comments from Curtis.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you Horatio.

I will merge your branch. in a few minutes. I will then start a new branch to reconcile the duplication of of the argument parser with two other parsers. I want to update the template you provide to provide the same maintained infrastructure as other jobs so that test authors only need to add args that the test will need. The env, and the collection of logs, and clean up or resources will be done by the infrastructure.

review: Approve (code)
986. By Horacio Durán

Merge master

987. By Horacio Durán

Added missing return

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'assess_status_output.py'
--- assess_status_output.py 1970-01-01 00:00:00 +0000
+++ assess_status_output.py 2015-07-13 18:48:12 +0000
@@ -0,0 +1,154 @@
1#!/usr/bin/python
2from __future__ import print_function
3
4__metaclass__ = type
5
6from base_asses import get_base_parser
7from status import StatusTester
8from jujupy import (
9 make_client,
10)
11from deploy_stack import (
12 boot_context,
13 prepare_environment,
14)
15
16
17def run_complete_status(client, status):
18 """run the complete set of tests possible for any StatusParser.
19
20 :param client: python juju client.
21 :type client: jujupy.EnvJujuClient
22 :param status: a BaseStatusParser
23 :type status: BaseStatusParser
24 """
25 status.s.assert_machines_len(2)
26 status.s.assert_machines_ids(("0", "1"))
27 juju_status = client.get_status()
28 for name, machine in juju_status.iter_machines(False):
29 status.s.assert_machine_agent_state(name, "started")
30 status.s.assert_machine_agent_version(name,
31 machine.get("agent-version"))
32 status.s.assert_machine_dns_name(name, machine.get("dns-name"))
33 status.s.assert_machine_instance_id(name,
34 machine.get("instance-id"))
35 status.s.assert_machine_series(name, machine.get("series"))
36 status.s.assert_machine_hardware(name, machine.get("hardware"))
37 state_server = machine.get("state-server-member-status", None)
38 if state_server:
39 status.s.assert_machine_member_status(name, "has-vote")
40
41 status.s.assert_service_charm("statusstresser",
42 "local:trusty/statusstresser-1")
43 status.s.assert_service_exposed("statusstresser", False)
44 status.s.assert_service_service_status("statusstresser",
45 {"current": "active",
46 "message": "called in "
47 "config-changed hook"})
48 status.s.assert_unit_workload_status("statusstresser/0",
49 {"current": "active",
50 "message": "called in "
51 "config-changed hook"})
52 status.s.assert_unit_agent_status("statusstresser/0",
53 {"current": "idle", "message": ""})
54 status.s.assert_unit_agent_state("statusstresser/0", "started")
55 agent_versions = juju_status.get_agent_versions()
56 for version in agent_versions:
57 for item in agent_versions[version]:
58 if not item.isdigit():
59 status.s.assert_unit_agent_version(item, version)
60 status.s.assert_unit_machine("statusstresser/0", "1")
61
62
63def run_reduced_status(client, status):
64 """run a subset of the status asserts.
65
66 run a reduced set of tests for a StatusParser, this is useful for
67 status outputs such as Tabular that hold less information.
68
69 :param client: python juju client.
70 :type client: jujupy.EnvJujuClient
71 :param status: a BaseStatusParser
72 :type status: BaseStatusParser
73 """
74 status.s.assert_machines_len(2)
75 status.s.assert_machines_ids(("0", "1"))
76 juju_status = client.get_status()
77 for name, machine in juju_status.iter_machines(False):
78 status.s.assert_machine_agent_state(name, "started")
79 status.s.assert_machine_agent_version(name,
80 machine.get("agent-version"))
81 status.s.assert_machine_dns_name(name, machine.get("dns-name"))
82 status.s.assert_machine_instance_id(name, machine.get("instance-id"))
83 status.s.assert_machine_series(name, machine.get("series"))
84 status.s.assert_machine_hardware(name, machine.get("hardware"))
85
86 status.s.assert_service_charm("statusstresser",
87 "local:trusty/statusstresser-1")
88 status.s.assert_service_exposed("statusstresser", False)
89 status.s.assert_service_service_status("statusstresser",
90 {"current": "active",
91 "message": ""})
92 status.s.assert_unit_workload_status("statusstresser/0",
93 {"current": "active",
94 "message": "called in "
95 "config-changed hook"})
96 status.s.assert_unit_agent_status("statusstresser/0",
97 {"current": "idle", "message": ""})
98 status.s.assert_unit_machine("statusstresser/0", "1")
99
100
101def test_status_set_on_install(client):
102 """Test the status after install.
103
104 Test that status set is proplerly called during install and
105 that all formats are returning proper information.
106
107 :param client: python juju client.
108 :type client: jujupy.EnvJujuClient
109 """
110 status = StatusTester.from_text(client.get_status(60, True,
111 "--format=yaml"),
112 "yaml")
113 run_complete_status(client, status)
114 status = StatusTester.from_text(client.get_status(60, True,
115 "--format=json"),
116 "json")
117 run_complete_status(client, status)
118 status = StatusTester.from_text(client.get_status(60, True,
119 "--format=tabular"),
120 "tabular")
121 run_reduced_status(client, status)
122
123
124def parse_args():
125 """Parse all arguments."""
126 parser = get_base_parser('Test status outputs')
127 return parser.parse_args()
128
129
130def main():
131 args = parse_args()
132 log_dir = args.logs
133
134 client = make_client(
135 args.juju_path, args.debug, args.env, args.temp_env_name)
136 # client.destroy_environment()
137 series = args.series
138 if series is None:
139 series = 'precise'
140 with boot_context(args.job_name, client, args.bootstrap_host,
141 args.machine, series, args.agent_url, args.agent_stream,
142 log_dir, args.keep_env, args.upload_tools):
143 prepare_environment(
144 client, already_bootstrapped=True, machines=args.machine)
145
146 client.get_status(60)
147 client.juju("deploy", ('local:trusty/statusstresser',))
148 client.wait_for_started()
149
150 test_status_set_on_install(client)
151
152
153if __name__ == '__main__':
154 main()
0155
=== added file 'base_asses.py'
--- base_asses.py 1970-01-01 00:00:00 +0000
+++ base_asses.py 2015-07-13 18:48:12 +0000
@@ -0,0 +1,58 @@
1from argparse import ArgumentParser
2from deploy_stack import add_juju_args, add_output_args
3from unittest import FunctionTestCase
4
5
6def get_base_parser(description=""):
7 """Return a parser with the base arguments required
8 for any asses_ test.
9
10 :param description: description for argument parser.
11 :type description: str
12 :returns: argument parser with base fields
13 :rtype: ArgumentParser
14 """
15 parser = ArgumentParser(description)
16 positional_args = [
17 ('env', 'The juju environment to base the temp test environment on.'),
18 ('juju_bin', 'Full path to the Juju binary.'),
19 ('logs', 'A directory in which to store logs.'),
20 ('temp_env_name', 'A temporary test environment name.'),
21 ]
22 for p_arg in positional_args:
23 name, help_txt = p_arg
24 parser.add_argument(name, help=help_txt)
25
26 parser.add_argument(
27 'juju_path', help='Directory your juju binary lives in.')
28 parser.add_argument(
29 'job_name', help='An arbitrary name for this job.')
30
31 add_juju_args(parser)
32 add_output_args(parser)
33
34 parser.add_argument('--upgrade', action="store_true", default=False,
35 help='Perform an upgrade test.')
36 parser.add_argument('--bootstrap-host',
37 help='The host to use for bootstrap.', default=None)
38 parser.add_argument('--machine', help='A machine to add or when used with '
39 'KVM based MaaS, a KVM image to start.',
40 action='append', default=[])
41 parser.add_argument('--keep-env', action='store_true', default=False,
42 help='Keep the Juju environment after the test'
43 ' completes.')
44 parser.add_argument(
45 '--upload-tools', action='store_true', default=False,
46 help='upload local version of tools before bootstrapping')
47 return parser
48
49
50def toUnitTest(testCase):
51 def unitTestify(*args, **kwargs):
52 tc = FunctionTestCase(testCase)
53 largs = list(args)
54 largs.insert(1, tc)
55 args = tuple(largs)
56 return testCase(*args, **kwargs)
57
58 return unitTestify
059
=== modified file 'jujupy.py'
--- jujupy.py 2015-06-16 20:18:37 +0000
+++ jujupy.py 2015-07-13 18:48:12 +0000
@@ -124,9 +124,9 @@
124 return self.get_env_client(environment).get_juju_output(124 return self.get_env_client(environment).get_juju_output(
125 command, *args, **kwargs)125 command, *args, **kwargs)
126126
127 def get_status(self, environment, timeout=60):127 def get_status(self, environment, timeout=60, raw=False, *args):
128 """Get the current status as a dict."""128 """Get the current status as a dict."""
129 return self.get_env_client(environment).get_status(timeout)129 return self.get_env_client(environment).get_status(timeout, raw, *args)
130130
131 def get_env_option(self, environment, option):131 def get_env_option(self, environment, option):
132 """Return the value of the environment's configured option."""132 """Return the value of the environment's configured option."""
@@ -297,10 +297,12 @@
297 print('!!! ' + e.stderr)297 print('!!! ' + e.stderr)
298 raise298 raise
299299
300 def get_status(self, timeout=60):300 def get_status(self, timeout=60, raw=False, *args):
301 """Get the current status as a dict."""301 """Get the current status as a dict."""
302 for ignored in until_timeout(timeout):302 for ignored in until_timeout(timeout):
303 try:303 try:
304 if raw:
305 return self.get_juju_output('status', *args)
304 return Status.from_text(self.get_juju_output('status'))306 return Status.from_text(self.get_juju_output('status'))
305 except subprocess.CalledProcessError as e:307 except subprocess.CalledProcessError as e:
306 pass308 pass
@@ -929,8 +931,8 @@
929 def juju(self, command, *args):931 def juju(self, command, *args):
930 return self.client.juju(self, command, args)932 return self.client.juju(self, command, args)
931933
932 def get_status(self, timeout=60):934 def get_status(self, timeout=60, raw=False, *args):
933 return self.client.get_status(self, timeout)935 return self.client.get_status(self, timeout, raw, *args)
934936
935 def wait_for_deploy_started(self, service_count=1, timeout=1200):937 def wait_for_deploy_started(self, service_count=1, timeout=1200):
936 """Wait until service_count services are 'started'.938 """Wait until service_count services are 'started'.
937939
=== added file 'status.py'
--- status.py 1970-01-01 00:00:00 +0000
+++ status.py 2015-07-13 18:48:12 +0000
@@ -0,0 +1,385 @@
1from __future__ import print_function
2
3__metaclass__ = type
4
5import json
6import re
7from base_asses import toUnitTest
8from jujupy import yaml_loads
9
10# Machine and unit, deprecated in unit
11AGENT_STATE_KEY = "agent-state"
12AGENT_VERSION_KEY = "agent-version"
13DNS_NAME_KEY = "dns-name"
14INSTANCE_ID_KEY = "instance-id"
15HARDWARE_KEY = "hardware"
16SERIES_KEY = "series"
17STATE_SERVER_MEMBER_STATUS_KEY = "state-server-member-status"
18# service
19CHARM_KEY = "charm"
20EXPOSED_KEY = "exposed"
21SERVICE_STATUS_KEY = "service-status"
22# unit
23WORKLOAD_STATUS_KEY = "workload-status"
24AGENT_STATUS_KEY = "agent-status"
25MACHINE_KEY = "machine"
26PUBLIC_ADDRESS_KEY = "public-address"
27
28MACHINE_TAB_HEADERS = ['ID', 'STATE', 'VERSION', 'DNS', 'INS-ID', 'SERIES',
29 'HARDWARE']
30UNIT_TAB_HEADERS = ['ID', 'WORKLOAD-STATE', 'AGENT-STATE', 'VERSION',
31 'MACHINE', 'PORTS', 'PUBLIC-ADDRESS', 'MESSAGE']
32SERVICE_TAB_HEADERS = ['NAME', 'STATUS', 'EXPOSED', 'CHARM']
33
34
35class StatusTester:
36 def __init__(self, text="", status_format="yaml"):
37 self._text = text
38 self._format = status_format
39 self.s = globals()["Status%sParser" % status_format.capitalize()](text)
40
41 @classmethod
42 def from_text(cls, text, status_format):
43 return cls(text, status_format)
44
45 def __unicode__(self):
46 return self._text
47
48 def __str__(self):
49 return self._text
50
51
52class ErrNoStatus(Exception):
53 """An exception for missing juju status."""
54
55
56class ErrMalformedStatus(Exception):
57 """An exception for unexpected formats of status."""
58
59
60class ErrUntestedStatusOutput(Exception):
61 """An exception for results returned by status.
62
63 Status that are known not to be covered by the tests should raise
64 this exception. """
65
66
67class BaseStatusParser:
68
69 _expected = set(["environment", "services", "machines"])
70
71 def __init__(self):
72 # expected entity storage
73 self._machines = dict()
74 self._services = dict()
75 self._units = dict()
76
77 self.parse()
78
79 def parse(self):
80 return self._parse()
81
82 @toUnitTest
83 def store(self, tc, parsed):
84 # Either there are less items than expected and therefore status
85 # is returning a subset of what it should or there are more and
86 # this means there are untested keys in status.
87 tc.assertItemsEqual(parsed.keys(), self._expected,
88 "untested items or incomplete status output")
89
90 # status of machines.
91 for machine_id, machine in parsed.get("machines", {}).iteritems():
92 tc.assertNotIn(machine_id, self._machines,
93 "Machine %s is repeated in yaml"
94 " status " % machine_id)
95 self._machines[machine_id] = machine
96
97 # status of services
98 for service_name, service in parsed.get("services", {}).iteritems():
99 tc.assertNotIn(service_name, self._services,
100 "Service %s is repeated in yaml "
101 "status " % service_name)
102 self._services[service_name] = service
103
104 # status of units
105 for service_name, service in self._services.iteritems():
106 for unit_name, unit in service.get("units", {}).iteritems():
107 tc.assertNotIn(unit_name, self._units,
108 "Unit %s is repeated in yaml "
109 "status " % unit_name)
110 self._units[unit_name] = unit
111
112 @toUnitTest
113 def assert_machines_len(self, tc, expected_len):
114 """Assert that we got as many machines as we where expecting.
115
116 :param expected_len: expected quantity of machines.
117 :type expected_len: int
118 """
119 tc.assertEqual(len(self._machines), expected_len)
120
121 @toUnitTest
122 def assert_machines_ids(self, tc, expected_ids):
123 """Assert that we got the machines we where expecting.
124
125 :param expected_ids: expected ids of machines.
126 :type expected_ids: tuple
127 """
128 tc.assertItemsEqual(self._machines, expected_ids)
129
130 def _machine_key_get(self, tc, machine_id, key):
131 tc.assertIn(machine_id, self._machines,
132 "Machine \"%s\" not present in machines" % machine_id)
133 tc.assertIn(key, self._machines[machine_id],
134 "Key \"%s\" not present in Machine \"%s\"" %
135 (key, machine_id))
136 return self._machines[machine_id][key]
137
138 @toUnitTest
139 def assert_machine_agent_state(self, tc, machine_id, state):
140 value = self._machine_key_get(tc, machine_id, AGENT_STATE_KEY)
141 tc.assertEqual(value, state)
142
143 @toUnitTest
144 def assert_machine_agent_version(self, tc, machine_id, version):
145 value = self._machine_key_get(tc, machine_id, AGENT_VERSION_KEY)
146 tc.assertEqual(value, version)
147
148 @toUnitTest
149 def assert_machine_dns_name(self, tc, machine_id, dns_name):
150 value = self._machine_key_get(tc, machine_id, DNS_NAME_KEY)
151 tc.assertEqual(value, dns_name)
152
153 @toUnitTest
154 def assert_machine_instance_id(self, tc, machine_id, instance_id):
155 value = self._machine_key_get(tc, machine_id, INSTANCE_ID_KEY)
156 tc.assertEqual(value, instance_id)
157
158 @toUnitTest
159 def assert_machine_series(self, tc, machine_id, series):
160 value = self._machine_key_get(tc, machine_id, SERIES_KEY)
161 tc.assertEqual(value, series)
162
163 @toUnitTest
164 def assert_machine_hardware(self, tc, machine_id, hardware):
165 value = self._machine_key_get(tc, machine_id, HARDWARE_KEY)
166 tc.assertEqual(value, hardware)
167
168 @toUnitTest
169 def assert_machine_member_status(self, tc, machine_id, member_status):
170 value = self._machine_key_get(tc, machine_id,
171 STATE_SERVER_MEMBER_STATUS_KEY)
172 tc.assertEqual(value, member_status)
173
174 def _service_key_get(self, tc, service_name, key):
175 tc.assertIn(service_name, self._services,
176 "Service \"%s\" not present in services." % service_name)
177 tc.assertIn(key, self._services[service_name],
178 "Key \"%s\" not present in Service \"%s\"" %
179 (key, service_name))
180 return self._services[service_name][key]
181
182 # Service status
183 @toUnitTest
184 def assert_service_charm(self, tc, service_name, charm):
185 value = self._service_key_get(tc, service_name, CHARM_KEY)
186 tc.assertEqual(value, charm)
187
188 @toUnitTest
189 def assert_service_exposed(self, tc, service_name, exposed):
190 value = self._service_key_get(tc, service_name, EXPOSED_KEY)
191 tc.assertEqual(value, exposed)
192
193 @toUnitTest
194 def assert_service_service_status(self, tc, service_name,
195 status={"current": "", "message": ""}):
196 value = self._service_key_get(tc, service_name, SERVICE_STATUS_KEY)
197 tc.assertEqual(value["current"], status["current"])
198 tc.assertEqual(value["message"], status["message"])
199
200 def _unit_key_get(self, tc, unit_name, key):
201 tc.assertIn(unit_name, self._units,
202 "Unit \"%s\" not present in units" % unit_name)
203 tc.assertIn(key, self._units[unit_name],
204 "Key \"%s\" not present in Unit \"%s\"" %
205 (key, unit_name))
206 return self._units[unit_name][key]
207
208 # Units status
209 @toUnitTest
210 def assert_unit_workload_status(self, tc, unit_name,
211 status={"current": "", "message": ""}):
212 value = self._unit_key_get(tc, unit_name, WORKLOAD_STATUS_KEY)
213 tc.assertEqual(value["current"], status["current"])
214 tc.assertEqual(value["message"], status["message"])
215
216 @toUnitTest
217 def assert_unit_agent_status(self, tc, unit_name,
218 status={"current": "", "message": ""}):
219 value = self._unit_key_get(tc, unit_name, AGENT_STATUS_KEY)
220 tc.assertEqual(value["current"], status["current"])
221 # Message is optional for unit agents.
222 tc.assertEqual(value.get("message", ""), status["message"])
223
224 @toUnitTest
225 def assert_unit_agent_state(self, tc, unit_name, state):
226 value = self._unit_key_get(tc, unit_name, AGENT_STATE_KEY)
227 tc.assertEqual(value, state)
228
229 @toUnitTest
230 def assert_unit_agent_version(self, tc, unit_name, version):
231 value = self._unit_key_get(tc, unit_name, AGENT_VERSION_KEY)
232 tc.assertEqual(value, version)
233
234 @toUnitTest
235 def assert_unit_machine(self, tc, unit_name, machine):
236 value = self._unit_key_get(tc, unit_name, MACHINE_KEY)
237 tc.assertEqual(value, machine)
238
239 @toUnitTest
240 def assert_unit_public_address(self, tc, unit_name, address):
241 value = self._unit_key_get(tc, unit_name, PUBLIC_ADDRESS_KEY)
242 tc.assertEqual(value, address)
243
244
245class StatusYamlParser(BaseStatusParser):
246 """StatusYamlParser handles parsing of status output in yaml format.
247
248 To be used by status tester.
249 """
250
251 def __init__(self, yaml=""):
252 self._yaml = yaml
253 if yaml == "":
254 raise ErrNoStatus("Yaml status was empty")
255 super(StatusYamlParser, self).__init__()
256
257 def _parse(self):
258 parsed = yaml_loads(self._yaml)
259 self.store(parsed)
260
261
262class StatusJsonParser(BaseStatusParser):
263 """StatusJSONParser handles parsing of status output in JSON format.
264
265 To be used by status tester.
266 """
267
268 def __init__(self, json_text=""):
269 self._json = json_text
270 if json_text == "":
271 raise ErrNoStatus("JSON status was empty")
272 super(StatusJsonParser, self).__init__()
273
274 def _parse(self):
275 parsed = json.loads(self._json)
276 self.store(parsed)
277
278
279class StatusTabularParser(BaseStatusParser):
280 """StatusTabularParser handles parsing of status output in Tabular format.
281
282 To be used by status tester.
283 """
284
285 def __init__(self, tabular_text=""):
286 self._tabular = tabular_text
287 if tabular_text == "":
288 raise ErrNoStatus("tabular status was empty")
289 super(StatusTabularParser, self).__init__()
290
291 @toUnitTest
292 def _normalize_machines(self, tc, header, items):
293 nitems = items[:6]
294 nitems.append(" ".join(items[6:]))
295 tc.assertEqual(header, MACHINE_TAB_HEADERS,
296 "Unexpected headers for machine:\n"
297 "wanted: %s"
298 "got: %s" % (MACHINE_TAB_HEADERS, header))
299 normalized = dict(zip((AGENT_STATE_KEY, AGENT_VERSION_KEY,
300 DNS_NAME_KEY, INSTANCE_ID_KEY,
301 SERIES_KEY, HARDWARE_KEY),
302 nitems[1:]))
303 return nitems[0], normalized
304
305 @toUnitTest
306 def _normalize_units(self, tc, header, items):
307 eid, wlstate, astate, version, machine, paddress = items[:6]
308 message = " ".join(items[6:])
309 wlstatus = {"current": wlstate, "message": message,
310 "since": "bogus date"}
311 astatus = {"current": astate, "message": "", "since": "bogus date"}
312 tc.assertEqual(header, UNIT_TAB_HEADERS,
313 "Unexpected headers for unit.\n"
314 "wanted: %s"
315 "got: %s" % (UNIT_TAB_HEADERS, header))
316 normalized = dict(zip((WORKLOAD_STATUS_KEY, AGENT_STATUS_KEY,
317 AGENT_VERSION_KEY, MACHINE_KEY,
318 PUBLIC_ADDRESS_KEY),
319 (wlstatus, astatus, version, machine, paddress)))
320
321 return eid, normalized
322
323 @toUnitTest
324 def _normalize_services(self, tc, header, items):
325 name, status, exposed, charm = items
326 tc.assertEqual(header, SERVICE_TAB_HEADERS,
327 "Unexpected headers for service.\n"
328 "wanted: %s"
329 "got: %s" % (SERVICE_TAB_HEADERS, header))
330 normalized = dict(zip((CHARM_KEY, EXPOSED_KEY, SERVICE_STATUS_KEY),
331 (charm, exposed == "true", {"current": status,
332 "message": ""})))
333 return name, normalized
334
335 def _parse(self):
336 section = re.compile("^\[(\w*)\]")
337 base = {"environment": "not provided"}
338 current_parent = ""
339 current_headers = []
340 prev_was_section = False
341 for line in self._tabular.splitlines():
342 # parse section
343 is_section = section.findall(line)
344 if len(is_section) == 1:
345 current_parent = is_section[0].lower()
346 if current_parent != "units":
347 base[current_parent] = {}
348 prev_was_section = True
349 continue
350 # parse headers
351 if prev_was_section:
352 prev_was_section = False
353 current_headers = line.split()
354 continue
355
356 # parse content
357 if current_parent == "" or current_headers == []:
358 raise ErrMalformedStatus("Tabular status is malformed")
359 items = line.split()
360
361 # separation line
362 if len(items) == 0:
363 continue
364
365 normalize = None
366 if current_parent == "services":
367 normalize = self._normalize_services
368 elif current_parent == "units":
369 normalize = self._normalize_units
370 elif current_parent == "machines":
371 normalize = self._normalize_machines
372
373 if not normalize:
374 raise ErrUntestedStatusOutput("%s is not an expected tabular"
375 " status section" %
376 current_parent)
377 k, v = normalize(current_headers, items)
378 if current_parent == "units":
379 base.setdefault("services", dict())
380 service = k.split("/")[0]
381 base["services"][service].setdefault("units", dict())
382 base["services"][service]["units"][k] = v
383 else:
384 base[current_parent][k] = v
385 self.store(base)
0386
=== added file 'template_assess.py.tmpl'
--- template_assess.py.tmpl 1970-01-01 00:00:00 +0000
+++ template_assess.py.tmpl 2015-07-13 18:48:12 +0000
@@ -0,0 +1,54 @@
1#!/usr/bin/python
2from __future__ import print_function
3
4__metaclass__ = type
5
6from base_asses import get_base_parser
7from status import StatusTester
8from jujupy import (
9 make_client,
10)
11from deploy_stack import (
12 boot_context,
13 prepare_environment,
14)
15
16TEST_HUMAN_NAME="<YOUR TEST HUMAN READABLE HERE>"
17
18
19def parse_args():
20 """Parse all arguments."""
21 parser = get_base_parser(TEST_HUMAN_NAME)
22 return parser.parse_args()
23
24
25def main():
26 args = parse_args()
27 log_dir = args.logs
28
29 client = make_client(
30 args.juju_path, args.debug, args.env_name, args.temp_env_name)
31 client.destroy_environment()
32 series = args.series
33 if series is None:
34 series = 'precise'
35 with boot_context(args.job_name, client, args.bootstrap_host,
36 args.machine, series, args.agent_url, args.agent_stream,
37 log_dir, args.keep_env, args.upload_tools):
38 prepare_environment(
39 client, already_bootstrapped=True, machines=args.machine)
40
41 client.get_status(60)
42 # Deploy charms, there are several under ./repository
43 client.juju("deploy", ('local:trusty/wordpress',))
44 # Wait for the deployment to finish.
45 client.wait_for_started()
46 #----------------- CALL YOUR TESTS HERE
47 # At this point you have a juju bootstraped with a wordpress charm
48 # deployed and active with the agent idle.
49
50 #----------------- TESTS END HERE
51
52
53if __name__ == '__main__':
54 main()
055
=== added file 'test_assess_status_output.py'
--- test_assess_status_output.py 1970-01-01 00:00:00 +0000
+++ test_assess_status_output.py 2015-07-13 18:48:12 +0000
@@ -0,0 +1,426 @@
1__metaclass__ = type
2
3from status import (
4 ErrNoStatus,
5 StatusYamlParser,
6 StatusJsonParser,
7 StatusTabularParser
8)
9
10from unittest import TestCase
11
12SAMPLE_YAML_OUTPUT = """environment: bogusec2
13machines:
14 "0":
15 agent-state: started
16 agent-version: 1.25-alpha1
17 dns-name: 54.82.51.4
18 instance-id: i-c0dadd10
19 instance-state: running
20 series: trusty
21 hardware: arch=amd64 cpu-cores=1 cpu-power=100 mem=1740M """ \
22 """root-disk=8192M availability-zone=us-east-1c
23 state-server-member-status: has-vote
24 "1":
25 agent-state: started
26 agent-version: 1.25-alpha1
27 dns-name: 54.162.95.230
28 instance-id: i-39280ac6
29 instance-state: running
30 series: trusty
31 hardware: arch=amd64 cpu-cores=1 cpu-power=100 mem=1740M """\
32 """root-disk=8192M availability-zone=us-east-1d
33services:
34 statusstresser:
35 charm: local:trusty/statusstresser-1
36 exposed: false
37 service-status:
38 current: active
39 message: called in config-changed hook
40 since: 12 Jun 2015 13:15:25-03:00
41 units:
42 statusstresser/0:
43 workload-status:
44 current: active
45 message: called in config-changed hook
46 since: 12 Jun 2015 13:15:25-03:00
47 agent-status:
48 current: idle
49 since: 12 Jun 2015 14:33:08-03:00
50 version: 1.25-alpha1
51 agent-state: started
52 agent-version: 1.25-alpha1
53 machine: "1"
54 public-address: 54.162.95.230
55"""
56# This could be replaced by a json.dumps({}) but the text is kept to
57# make this test about status output as accurate as possible.
58SAMPLE_JSON_OUTPUT = \
59 """{"environment":"perritoec2","machines":{"0":{"agent-state":"started","""\
60 """"agent-version":"1.25-alpha1","dns-name":"54.82.51.4","""\
61 """"instance-id":"i-c0dadd10","instance-state":"running","""\
62 """"series":"trusty","hardware":"arch=amd64 cpu-cores=1 cpu-power=100 """\
63 """mem=1740M root-disk=8192M availability-zone=us-east-1c","""\
64 """"state-server-member-status":"has-vote"},"1":{"agent-state":"""\
65 """"started","agent-version":"1.25-alpha1","dns-name":"54.162.95.230","""\
66 """"instance-id":"i-a7a2b377","instance-state":"running","""\
67 """"series":"trusty","hardware":"arch=amd64 cpu-cores=1 cpu-power=300 """\
68 """mem=3840M root-disk=8192M availability-zone=us-east-1c"}},"""\
69 """"services":{"statusstresser":{"charm": """\
70 """"local:trusty/statusstresser-1","exposed":false,"service-status":"""\
71 """{"current":"active","message":"called in config-changed hook","""\
72 """"since":"15 Jun 2015 20:56:29-03:00"},"""\
73 """"units":{"statusstresser/0":{"workload-status":"""\
74 """{"current":"active","message":"called in config-changed hook","""\
75 """"since":"15 Jun 2015 20:56:29-03:00"},"agent-status":"""\
76 """{"current":"idle","since":"15 Jun 2015 20:56:41-03:00","""\
77 """"version":"1.25-alpha1"},"agent-state":"started","agent-version":"""\
78 """"1.25-alpha1","machine":"1","public-address":"54.162.95.230"}}}}}"""
79
80SAMPLE_TABULAR_OUTPUT = """[Services]
81NAME STATUS EXPOSED CHARM
82statusstresser active false local:trusty/statusstresser-1
83
84[Units]
85ID WORKLOAD-STATE AGENT-STATE VERSION MACHINE PORTS """\
86"""PUBLIC-ADDRESS MESSAGE
87statusstresser/0 active idle 1.25-alpha1 1 """\
88"""54.162.95.230 called in config-changed hook
89
90[Machines]
91ID STATE VERSION DNS INS-ID SERIES HARDWARE
920 started 1.25-alpha1 54.82.51.4 i-c0dadd10 trusty arch=amd64 """\
93 """cpu-cores=1 cpu-power=100 mem=1740M root-disk=8192M """\
94 """availability-zone=us-east-1c
951 started 1.25-alpha1 54.162.95.230 i-39280ac6 trusty arch=amd64 """\
96 """cpu-cores=1 cpu-power=100 mem=1740M root-disk=8192M """\
97 """availability-zone=us-east-1d"""
98
99
100class ReducedTestStatus:
101
102 def test_assert_machine_ids(self):
103 self.parser.assert_machines_ids(["0", "1"])
104
105 def test_assert_machine_ids_failed(self):
106 with self.assertRaises(AssertionError):
107 self.parser.assert_machines_ids(["0", "1", "2"])
108
109 def test_assert_machine_len(self):
110 self.parser.assert_machines_len(2)
111
112 def test_assert_machine_len_failed(self):
113 with self.assertRaises(AssertionError):
114 self.parser.assert_machines_len(3)
115
116 def test_machine_agent_state_valid(self):
117 self.parser.assert_machine_agent_state("0", "started")
118
119 def test_machine_agent_state_failed(self):
120 with self.assertRaises(AssertionError):
121 self.parser.assert_machine_agent_state("0", "stopped")
122
123 def test_machine_agent_state_error(self):
124 with self.assertRaises(AssertionError):
125 self.parser.assert_machine_agent_state("3", "stopped")
126
127 def test_assert_machine_agent_version(self):
128 self.parser.assert_machine_agent_version("0", "1.25-alpha1")
129
130 def test_assert_machine_agent_version_failed(self):
131 with self.assertRaises(AssertionError):
132 self.parser.assert_machine_agent_version("0", "1.25-alpha2")
133
134 def test_assert_machine_agent_version_error(self):
135 with self.assertRaises(AssertionError):
136 self.parser.assert_machine_agent_version("5", "1.25-alpha1")
137
138 def test_assert_machine_dns_name(self):
139 self.parser.assert_machine_dns_name("0", "54.82.51.4")
140
141 def test_assert_machine_dns_name_failed(self):
142 with self.assertRaises(AssertionError):
143 self.parser.assert_machine_dns_name("0", "54.82.51.5")
144
145 def test_assert_machine_dns_name_error(self):
146 with self.assertRaises(AssertionError):
147 self.parser.assert_machine_dns_name("3", "54.82.51.4")
148
149 def test_assert_machine_instance_id(self):
150 self.parser.assert_machine_instance_id("0", "i-c0dadd10")
151
152 def test_assert_machine_instance_id_failed(self):
153 with self.assertRaises(AssertionError):
154 self.parser.assert_machine_instance_id("0", "i-c0dadd11")
155
156 def test_assert_machine_instance_id_error(self):
157 with self.assertRaises(AssertionError):
158 self.parser.assert_machine_instance_id("3", "i-c0dadd10")
159
160 def test_assert_machine_series(self):
161 self.parser.assert_machine_series("0", "trusty")
162
163 def test_assert_machine_series_failed(self):
164 with self.assertRaises(AssertionError):
165 self.parser.assert_machine_series("0", "utopic")
166
167 def test_assert_machine_series_error(self):
168 with self.assertRaises(AssertionError):
169 self.parser.assert_machine_series("3", "trusty")
170
171 def test_assert_machine_hardware(self):
172 self.parser.assert_machine_hardware("0", "arch=amd64 cpu-cores=1 "
173 "cpu-power=100 mem=1740M "
174 "root-disk=8192M "
175 "availability-zone="
176 "us-east-1c")
177
178 def test_assert_machine_hardware_failed(self):
179 with self.assertRaises(AssertionError):
180 self.parser.assert_machine_hardware("0", "arch=arm cpu-cores=1 "
181 "cpu-power=100 mem=1740M "
182 "root-disk=8192M "
183 "availability-zone="
184 "us-east-1c")
185
186 def test_assert_machine_hardware_error(self):
187 with self.assertRaises(AssertionError):
188 self.parser.assert_machine_hardware("3", "arch=amd64 cpu-cores=1 "
189 "cpu-power=100 mem=1740M "
190 "root-disk=8192M "
191 "availability-zone="
192 "us-east-1c")
193
194 def test_assert_service_charm(self):
195 self.parser.assert_service_charm("statusstresser",
196 "local:trusty/statusstresser-1")
197
198 def test_assert_service_charm_failed(self):
199 with self.assertRaises(AssertionError):
200 self.parser.assert_service_charm("statusstresser",
201 "local:trusty/statusstresser-2")
202
203 def test_assert_service_charm_error(self):
204 with self.assertRaises(AssertionError):
205 self.parser.assert_service_charm("statusrelaxer",
206 "local:trusty/statusstresser-1")
207
208 def test_assert_service_exposed(self):
209 self.parser.assert_service_exposed("statusstresser", False)
210
211 def test_assert_service_exposed_failed(self):
212 with self.assertRaises(AssertionError):
213 self.parser.assert_service_exposed("statusstresser", True)
214
215 def test_assert_service_exposed_error(self):
216 with self.assertRaises(AssertionError):
217 self.parser.assert_service_exposed("statusrelaxer", False)
218
219 def test_assert_unit_public_address(self):
220 self.parser.assert_unit_public_address("statusstresser/0",
221 "54.162.95.230")
222
223 def test_assert_unit_public_address_failed(self):
224 with self.assertRaises(AssertionError):
225 self.parser.assert_unit_public_address("statusstresser/0",
226 "54.162.95.231")
227
228 def test_assert_unit_public_address_error(self):
229 with self.assertRaises(AssertionError):
230 self.parser.assert_unit_public_address("statusrelaxer/0",
231 "54.162.95.230")
232
233
234class BaseTestStatus(ReducedTestStatus):
235
236 def test_assert_machine_member_status(self):
237 self.parser.assert_machine_member_status("0", "has-vote")
238
239 def test_assert_machine_member_status_failed(self):
240 with self.assertRaises(AssertionError):
241 self.parser.assert_machine_member_status("0", "not-voting")
242
243 def test_assert_machine_member_status_error(self):
244 with self.assertRaises(AssertionError):
245 self.parser.assert_machine_member_status("3", "has-vote")
246
247 def test_assert_service_service_status(self):
248 self.parser.assert_service_service_status("statusstresser",
249 {"current": "active",
250 "message": "called in "
251 "config-changed hook"})
252
253 def test_assert_service_service_status_failed(self):
254 with self.assertRaises(AssertionError):
255 self.parser.assert_service_service_status("statusstresser",
256 {"current": "active",
257 "message": "another "
258 "message"})
259
260 def test_assert_service_service_status_error(self):
261 with self.assertRaises(AssertionError):
262 self.parser.assert_service_service_status("statusrelaxer",
263 {"current": "active",
264 "message": "called in "
265 "config-changed hook"})
266
267 def test_assert_unit_workload_status(self):
268 self.parser.assert_unit_workload_status("statusstresser/0",
269 {"current": "active",
270 "message": "called in "
271 "config-changed hook"})
272
273 def test_assert_unit_workload_status_failed(self):
274 with self.assertRaises(AssertionError):
275 self.parser.assert_unit_workload_status("statusstresser/0",
276 {"current": "active",
277 "message": "another "
278 "message"})
279
280 def test_assert_unit_workload_status_error(self):
281 with self.assertRaises(AssertionError):
282 self.parser.assert_unit_workload_status("statusrelaxer/0",
283 {"current": "active",
284 "message": "called in "
285 "config-changed hook"})
286
287 def test_assert_unit_agent_status(self):
288 self.parser.assert_unit_agent_status("statusstresser/0",
289 {"current": "idle",
290 "message": ""})
291
292 def test_assert_unit_agent_status_failed(self):
293 with self.assertRaises(AssertionError):
294 self.parser.assert_unit_agent_status("statusstresser/0",
295 {"current": "idle",
296 "message": "an unexpected "
297 "message"})
298
299 def test_assert_unit_agent_status_error(self):
300 with self.assertRaises(AssertionError):
301 self.parser.assert_unit_agent_status("statusrelaxer/0",
302 {"current": "idle",
303 "message": ""})
304
305 def test_assert_unit_agent_state(self):
306 self.parser.assert_unit_agent_state("statusstresser/0", "started")
307
308 def test_assert_unit_agent_state_failed(self):
309 with self.assertRaises(AssertionError):
310 self.parser.assert_unit_agent_state("statusstresser/0", "stopped")
311
312 def test_assert_unit_agent_state_error(self):
313 with self.assertRaises(AssertionError):
314 self.parser.assert_unit_agent_state("statusrelaxer/0", "started")
315
316 def test_assert_unit_agent_version(self):
317 self.parser.assert_unit_agent_version("statusstresser/0",
318 "1.25-alpha1")
319
320 def test_assert_unit_agent_version_failed(self):
321 with self.assertRaises(AssertionError):
322 self.parser.assert_unit_agent_version("statusstresser/0",
323 "1.25-alpha2")
324
325 def test_assert_unit_agent_version_error(self):
326 with self.assertRaises(AssertionError):
327 self.parser.assert_unit_agent_version("statusrelaxer/0",
328 "1.25-alpha1")
329
330 def test_assert_unit_machine(self):
331 self.parser.assert_unit_machine("statusstresser/0", "1")
332
333 def test_assert_unit_machine_failed(self):
334 with self.assertRaises(AssertionError):
335 self.parser.assert_unit_machine("statusstresser/0", "2")
336
337 def test_assert_unit_machine_error(self):
338 with self.assertRaises(AssertionError):
339 self.parser.assert_unit_machine("statusrelaxer/0", "1")
340
341
342class TestStatusForYaml(TestCase, BaseTestStatus):
343
344 def test_empty_yaml_fails(self):
345 with self.assertRaises(ErrNoStatus):
346 StatusYamlParser(yaml="")
347
348 def setUp(self):
349 self.parser = StatusYamlParser(yaml=SAMPLE_YAML_OUTPUT)
350
351
352class TestStatusForJson(TestCase, BaseTestStatus):
353
354 def test_empty_json_fails(self):
355 with self.assertRaises(ErrNoStatus):
356 StatusJsonParser(json_text="")
357
358 def setUp(self):
359 self.parser = StatusJsonParser(json_text=SAMPLE_JSON_OUTPUT)
360
361
362class TestStatusTabular(TestCase, ReducedTestStatus):
363
364 def test_empty_tabular_fails(self):
365 with self.assertRaises(ErrNoStatus):
366 StatusTabularParser("")
367
368 def setUp(self):
369 self.parser = StatusTabularParser(tabular_text=SAMPLE_TABULAR_OUTPUT)
370
371 def test_assert_service_service_status(self):
372 self.parser.assert_service_service_status("statusstresser",
373 {"current": "active",
374 "message": ""})
375
376 def test_assert_service_service_status_failed(self):
377 with self.assertRaises(AssertionError):
378 self.parser.assert_service_service_status("statusstresser",
379 {"current": "active",
380 "message": "another "
381 "message"})
382
383 def test_assert_service_service_status_error(self):
384 with self.assertRaises(AssertionError):
385 self.parser.assert_service_service_status("statusrelaxer",
386 {"current": "active",
387 "message": "called in "
388 "config-changed hook"})
389
390 def test_assert_unit_workload_status(self):
391 self.parser.assert_unit_workload_status("statusstresser/0",
392 {"current": "active",
393 "message": "called in "
394 "config-changed hook"})
395
396 def test_assert_unit_workload_status_failed(self):
397 with self.assertRaises(AssertionError):
398 self.parser.assert_unit_workload_status("statusstresser/0",
399 {"current": "active",
400 "message": "another "
401 "message"})
402
403 def test_assert_unit_workload_status_error(self):
404 with self.assertRaises(AssertionError):
405 self.parser.assert_unit_workload_status("statusrelaxer/0",
406 {"current": "active",
407 "message": "called in "
408 "config-changed hook"})
409
410 def test_assert_unit_agent_status(self):
411 self.parser.assert_unit_agent_status("statusstresser/0",
412 {"current": "idle",
413 "message": ""})
414
415 def test_assert_unit_agent_status_failed(self):
416 with self.assertRaises(AssertionError):
417 self.parser.assert_unit_agent_status("statusstresser/0",
418 {"current": "idle",
419 "message": "an unexpected "
420 "message"})
421
422 def test_assert_unit_agent_status_error(self):
423 with self.assertRaises(AssertionError):
424 self.parser.assert_unit_agent_status("statusrelaxer/0",
425 {"current": "idle",
426 "message": ""})

Subscribers

People subscribed via source and target branches