Merge lp:~nskaggs/juju-ci-tools/add-assess-budget into lp:juju-ci-tools

Proposed by Nicholas Skaggs
Status: Merged
Merged at revision: 1890
Proposed branch: lp:~nskaggs/juju-ci-tools/add-assess-budget
Merge into: lp:juju-ci-tools
Diff against target: 545 lines (+536/-0)
2 files modified
assess_budget.py (+254/-0)
tests/test_assess_budget.py (+282/-0)
To merge this branch: bzr merge lp:~nskaggs/juju-ci-tools/add-assess-budget
Reviewer Review Type Date Requested Status
Christopher Lee (community) Approve
Review via email: mp+317406@code.launchpad.net

Description of the change

This change adds test for the budget commands within juju. Note the following bugs which affect the ability of this test to run. bug 1665013 and bug 1663258

You will need to authenticate to jujucharms.com using charm login, or setting the macaroon yourself with juju.

To post a comment you must log in.
Revision history for this message
Christopher Lee (veebers) wrote :

Please see inline comments.

review: Needs Fixing
1899. By Nicholas Skaggs

addressed review comments

Revision history for this message
Nicholas Skaggs (nskaggs) wrote :

See inline comments. I'm going to have a look at the whole number vs float issue.

Revision history for this message
Christopher Lee (veebers) wrote :

Replied to the incline comment replies.

Revision history for this message
Christopher Lee (veebers) wrote :

LGTM, a couple of comments re: test naming but nothing blocker.

review: Approve
1900. By Nicholas Skaggs

final review tweaks

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'assess_budget.py'
2--- assess_budget.py 1970-01-01 00:00:00 +0000
3+++ assess_budget.py 2017-02-16 21:51:25 +0000
4@@ -0,0 +1,254 @@
5+#!/usr/bin/env python
6+"""
7+This tests the budget commands utilized for commercial charm billing.
8+These commands are linked to a ubuntu sso account, and as such, require the
9+user account to be setup before test execution (including authentication).
10+You can use charm login to do this, or let juju authenticate with a browser.
11+"""
12+
13+from __future__ import print_function
14+
15+import argparse
16+import json
17+import logging
18+from random import randint
19+import subprocess
20+import sys
21+
22+
23+from jujupy import (
24+ client_from_config,
25+)
26+from utility import (
27+ add_basic_testing_arguments,
28+ JujuAssertionError,
29+ configure_logging,
30+)
31+
32+__metaclass__ = type
33+
34+
35+log = logging.getLogger("assess_budget")
36+
37+
38+def _get_new_budget_limit(client):
39+ """Return availible limit for new budget"""
40+ budgets = json.loads(list_budgets(client))
41+ limit = int(budgets['total']['limit'])
42+ credit = int(budgets['credit'])
43+ log.debug('Found credit limit {}, currently used {}'.format(
44+ credit, limit))
45+ return credit - limit
46+
47+
48+def _get_budgets(client):
49+ return json.loads(list_budgets(client))['budgets']
50+
51+
52+def _set_budget_value_expectations(expected_budgets, name, value):
53+ # Update our expectations accordingly
54+ for budget in expected_budgets:
55+ if budget['budget'] == name:
56+ # For now, we assume we aren't spending down the budget
57+ budget['limit'] = value
58+ budget['unallocated'] = value
59+ # .00 is appended to availible for some reason
60+ budget['available'] = '{:.2f}'.format(float(value))
61+ log.info('Expected budget updated: "{}" to {}'.format(name, value))
62+
63+
64+def _try_setting_budget(client, name, value):
65+ try:
66+ output = set_budget(client, name, value)
67+ except subprocess.CalledProcessError as e:
68+ output = [e.output, getattr(e, 'stderr', '')]
69+ raise JujuAssertionError('Could not set budget {}'.format(output))
70+
71+ if 'budget limit updated' not in output:
72+ raise JujuAssertionError('Error calling set-budget {}'.format(output))
73+
74+
75+def _try_creating_budget(client, name, value):
76+ try:
77+ create_budget(client, name, value)
78+ log.info('Created new budget "{}" with value {}'.format(name,
79+ value))
80+ except subprocess.CalledProcessError as e:
81+ output = [e.output, getattr(e, 'stderr', '')]
82+ if any('already exists' in message for message in output):
83+ log.info('Reusing budget "{}" with value {}'.format(name, value))
84+ pass # this will be a failure once lp:1663258 is fixed
85+ else:
86+ raise JujuAssertionError(
87+ 'Error testing create-budget: {}'.format(output))
88+ except:
89+ raise JujuAssertionError('Added duplicate budget')
90+
91+
92+def _try_greater_than_limit_budget(client, name, limit):
93+ error_strings = {
94+ 'pass': 'exceed the credit limit',
95+ 'unknown': 'Error testing budget greater than credit limit',
96+ 'fail': 'Credit limit exceeded'
97+ }
98+ over_limit_value = str(limit + randint(1, 100))
99+ assert_set_budget(client, name, over_limit_value, error_strings)
100+
101+
102+def _try_negative_budget(client, name):
103+ error_strings = {
104+ 'pass': 'Could not set budget',
105+ 'unknown': 'Error testing negative budget',
106+ 'fail': 'Negative budget allowed'
107+ }
108+ negative_budget_value = str(randint(-1000, -1))
109+ assert_set_budget(client, name, negative_budget_value, error_strings)
110+
111+
112+def assert_sorted_equal(found, expected):
113+ found = sorted(found)
114+ expected = sorted(expected)
115+ if found != expected:
116+ raise JujuAssertionError(
117+ 'Found: {}\nExpected: {}'.format(found, expected))
118+
119+
120+def assert_set_budget(client, name, limit, error_strings):
121+ try:
122+ _try_setting_budget(client, name, limit)
123+ except JujuAssertionError as e:
124+ if error_strings['pass'] not in e.message:
125+ raise JujuAssertionError(
126+ '{}: {}'.format(error_strings['unknown'], e))
127+ else:
128+ raise JujuAssertionError(error_strings['fail'])
129+
130+
131+def create_budget(client, name, value):
132+ """Create a budget"""
133+ return client.get_juju_output('create-budget', name, value,
134+ include_e=False)
135+
136+
137+def list_budgets(client):
138+ """Return defined budgets as json."""
139+ return client.get_juju_output('list-budgets', '--format', 'json',
140+ include_e=False)
141+
142+
143+def set_budget(client, name, value):
144+ """Change an existing budgets allocation."""
145+ return client.get_juju_output('set-budget', name, value, include_e=False)
146+
147+
148+def show_budget(client, name):
149+ """Return specified budget as json."""
150+ return client.get_juju_output('show-budget', name, '--format', 'json',
151+ include_e=False)
152+
153+
154+def assess_budget(client):
155+ # Since we can't remove budgets until lp:1663258
156+ # is fixed, we avoid creating new random budgets and hardcode.
157+ # We also, zero out the previous budget
158+ budget_name = 'test'
159+ _try_setting_budget(client, budget_name, '0')
160+
161+ budget_limit = _get_new_budget_limit(client)
162+ assess_budget_limit(budget_limit)
163+
164+ expected_budgets = _get_budgets(client)
165+ budget_value = str(randint(1, budget_limit / 2))
166+ assess_create_budget(client, budget_name, budget_value, budget_limit)
167+
168+ budget_value = str(randint(budget_limit / 2 + 1, budget_limit))
169+ assess_set_budget(client, budget_name, budget_value, budget_limit)
170+ assess_show_budget(client, budget_name, budget_value)
171+
172+ _set_budget_value_expectations(expected_budgets, budget_name, budget_value)
173+ assess_list_budgets(client, expected_budgets)
174+
175+
176+def assess_budget_limit(budget_limit):
177+ log.info('Assessing budget limit {}'.format(budget_limit))
178+
179+ if budget_limit < 0:
180+ raise JujuAssertionError(
181+ 'Negative Budget Limit {}'.format(budget_limit))
182+
183+
184+def assess_create_budget(client, budget_name, budget_value, budget_limit):
185+ """Test create-budget command"""
186+ log.info('create-budget "{}" with value {}, limit {}'.format(budget_name,
187+ budget_value,
188+ budget_limit))
189+
190+ # Do this twice, to ensure budget exists and we can check for
191+ # duplicate message. Ideally, once lp:1663258 is fixed, we will
192+ # assert on initial budget creation as well.
193+ _try_creating_budget(client, budget_name, budget_value)
194+
195+ log.info('Trying duplicate create-budget')
196+ _try_creating_budget(client, budget_name, budget_value)
197+
198+
199+def assess_list_budgets(client, expected_budgets):
200+ log.info('list-budgets testing expected values')
201+ # Since we can't remove budgets until lp:1663258
202+ # is fixed, we don't modify the list contents or count
203+ # Nonetheless, we assert on it for future use
204+ budgets = _get_budgets(client)
205+ assert_sorted_equal(budgets, expected_budgets)
206+
207+
208+def assess_set_budget(client, budget_name, budget_value, budget_limit):
209+ """Test set-budget command"""
210+ log.info('set-budget "{}" with value {}, limit {}'.format(budget_name,
211+ budget_value,
212+ budget_limit))
213+ _try_setting_budget(client, budget_name, budget_value)
214+
215+ # Check some bounds
216+ # Since budgetting is important, and the functional test is cheap,
217+ # let's test some basic bounds
218+ log.info('Trying set-budget with value greater than budget limit')
219+ _try_greater_than_limit_budget(client, budget_name, budget_limit)
220+
221+ log.info('Trying set-budget with negative value')
222+ _try_negative_budget(client, budget_name)
223+
224+
225+def assess_show_budget(client, budget_name, budget_value):
226+ log.info('show-budget "{}" with value {}'.format(budget_name,
227+ budget_value))
228+
229+ budget = json.loads(show_budget(client, budget_name))
230+
231+ # assert budget value
232+ if budget['limit'] != budget_value:
233+ raise JujuAssertionError('Budget limit found {}, expected {}'.format(
234+ budget['limit'], budget_value))
235+
236+ # assert on usage (0% until we use it)
237+ if budget['total']['usage'] != '0%':
238+ raise JujuAssertionError('Budget usage found {}, expected {}'.format(
239+ budget['total']['usage'], '0%'))
240+
241+
242+def parse_args(argv):
243+ """Parse all arguments."""
244+ parser = argparse.ArgumentParser(description="Test budget commands")
245+ add_basic_testing_arguments(parser)
246+ return parser.parse_args(argv)
247+
248+
249+def main(argv=None):
250+ args = parse_args(argv)
251+ configure_logging(args.verbose)
252+ client = client_from_config(args.env, args.juju_bin, False)
253+ assess_budget(client)
254+ return 0
255+
256+
257+if __name__ == '__main__':
258+ sys.exit(main())
259
260=== added file 'tests/test_assess_budget.py'
261--- tests/test_assess_budget.py 1970-01-01 00:00:00 +0000
262+++ tests/test_assess_budget.py 2017-02-16 21:51:25 +0000
263@@ -0,0 +1,282 @@
264+"""Tests for assess_budget module."""
265+
266+import logging
267+import json
268+from random import randint
269+import StringIO
270+from subprocess import CalledProcessError
271+
272+from mock import (
273+ Mock,
274+ patch,
275+)
276+
277+from assess_budget import (
278+ _try_greater_than_limit_budget,
279+ _try_negative_budget,
280+ assess_budget,
281+ assess_budget_limit,
282+ assess_create_budget,
283+ assess_list_budgets,
284+ assess_set_budget,
285+ assess_show_budget,
286+ main,
287+ parse_args,
288+)
289+from jujupy import (
290+ fake_juju_client,
291+)
292+from tests import (
293+ parse_error,
294+ TestCase,
295+)
296+from utility import (
297+ JujuAssertionError,
298+)
299+
300+
301+class TestParseArgs(TestCase):
302+
303+ def test_common_args(self):
304+ args = parse_args(["an-env", "/bin/juju", "/tmp/logs", "an-env-mod"])
305+ self.assertEqual("an-env", args.env)
306+ self.assertEqual("/bin/juju", args.juju_bin)
307+ self.assertEqual("/tmp/logs", args.logs)
308+ self.assertEqual("an-env-mod", args.temp_env_name)
309+ self.assertEqual(False, args.debug)
310+
311+ def test_help(self):
312+ fake_stdout = StringIO.StringIO()
313+ with parse_error(self) as fake_stderr:
314+ with patch("sys.stdout", fake_stdout):
315+ parse_args(["--help"])
316+ self.assertEqual("", fake_stderr.getvalue())
317+
318+
319+class TestMain(TestCase):
320+
321+ def test_main(self):
322+ argv = ["an-env", "/bin/juju", "/tmp/logs", "an-env-mod", "--verbose"]
323+ client = Mock(spec=["is_jes_enabled"])
324+ with patch('assess_budget.subprocess.check_output',
325+ autospec=True):
326+ with patch("assess_budget.configure_logging",
327+ autospec=True) as mock_cl:
328+ with patch('assess_budget.client_from_config',
329+ return_value=client) as mock_cfc:
330+ with patch("assess_budget.assess_budget",
331+ autospec=True) as mock_assess:
332+ main(argv)
333+ mock_cl.assert_called_once_with(logging.DEBUG)
334+ mock_cfc.assert_called_once_with('an-env', "/bin/juju", False)
335+ mock_assess.assert_called_once_with(client)
336+
337+
338+class TestAssess(TestCase):
339+
340+ def setUp(self):
341+ super(TestAssess, self).setUp()
342+ self.fake_client = fake_juju_client()
343+ self.budget_name = 'test'
344+ self.budget_limit = randint(1000, 10000)
345+ self.budget_value = str(randint(100, 900))
346+
347+ def test_assess_budget(self):
348+ show_b = patch("assess_budget.assess_show_budget",
349+ autospec=True)
350+ list_b = patch("assess_budget.assess_list_budgets",
351+ autospec=True)
352+ set_b = patch("assess_budget.assess_set_budget",
353+ autospec=True)
354+ create_b = patch("assess_budget.assess_create_budget",
355+ autospec=True)
356+ b_limit = patch("assess_budget.assess_budget_limit",
357+ autospec=True)
358+ init_b = patch("assess_budget._try_setting_budget",
359+ autospec=True)
360+ expect_b = patch("assess_budget._set_budget_value_expectations",
361+ autospec=True)
362+ update_e = patch("assess_budget._get_budgets", autospec=True)
363+
364+ with show_b as show_b_mock, list_b as list_b_mock, \
365+ set_b as set_b_mock, create_b as create_b_mock, \
366+ b_limit as b_limit_mock, init_b as init_b_mock, \
367+ expect_b as expect_b_mock, update_e as update_e_mock:
368+ with patch("assess_budget.json.loads"):
369+ with patch("assess_budget.randint",
370+ return_value=self.budget_value):
371+ assess_budget(self.fake_client)
372+
373+ init_b_mock.assert_called_once_with(self.fake_client,
374+ self.budget_name,
375+ '0')
376+ expect_b_mock.assert_called_once_with(
377+ update_e_mock(self.fake_client),
378+ self.budget_name, self.budget_value)
379+ b_limit_mock.assert_called_once_with(0)
380+ create_b_mock.assert_called_once_with(
381+ self.fake_client, self.budget_name,
382+ self.budget_value, 0)
383+ set_b_mock.assert_called_once_with(
384+ self.fake_client, self.budget_name,
385+ self.budget_value, 0)
386+ show_b_mock.assert_called_once_with(
387+ self.fake_client, self.budget_name,
388+ self.budget_value)
389+ list_b_mock.assert_called_once_with(
390+ self.fake_client, update_e_mock(self.fake_client))
391+
392+ self.assertEqual(init_b_mock.call_count, 1)
393+ self.assertEqual(expect_b_mock.call_count, 1)
394+ self.assertEqual(b_limit_mock.call_count, 1)
395+ self.assertEqual(create_b_mock.call_count, 1)
396+ self.assertEqual(set_b_mock.call_count, 1)
397+ self.assertEqual(show_b_mock.call_count, 1)
398+ self.assertEqual(list_b_mock.call_count, 1)
399+
400+
401+class TestAssessShowBudget(TestAssess):
402+
403+ def setUp(self):
404+ super(TestAssessShowBudget, self).setUp()
405+ self.fake_json = json.loads('{"limit":"' + self.budget_value +
406+ '","total":{"usage":"0%"}}')
407+
408+ def test_assess_show_budget(self):
409+ with patch.object(self.fake_client, 'get_juju_output'):
410+ with patch("assess_budget.json.loads",
411+ return_value=self.fake_json):
412+ assess_show_budget(self.fake_client, self.budget_name,
413+ self.budget_value)
414+
415+ def test_raises_budget_usage_error(self):
416+ error_usage = randint(1, 100)
417+ self.fake_json['total']['usage'] = error_usage
418+ with patch.object(self.fake_client, 'get_juju_output'):
419+ with patch("assess_budget.json.loads",
420+ return_value=self.fake_json):
421+ with self.assertRaises(JujuAssertionError) as ex:
422+ assess_show_budget(self.fake_client, self.budget_name,
423+ self.budget_value)
424+ self.assertEqual(ex.exception.message,
425+ 'Budget usage found {}, expected 0%'.format(
426+ error_usage))
427+
428+ def test_raises_budget_limit_error(self):
429+ self.fake_json['limit'] = 0
430+ with patch.object(self.fake_client, 'get_juju_output'):
431+ with patch("assess_budget.json.loads",
432+ return_value=self.fake_json):
433+ with self.assertRaises(JujuAssertionError) as ex:
434+ assess_show_budget(self.fake_client, self.budget_name,
435+ self.budget_value)
436+ self.assertEqual(ex.exception.message,
437+ 'Budget limit found 0, expected {}'.format(
438+ self.budget_value))
439+
440+
441+class TestAssessListBudgets(TestAssess):
442+
443+ def setUp(self):
444+ super(TestAssessListBudgets, self).setUp()
445+ snippet = '[{"budget": "test", "limit": "300"}]'
446+ self.fake_budgets_json = json.loads('{"budgets":' + snippet + '}')
447+ self.fake_budget_json = json.loads(snippet)
448+ unexpected_snippet = '[{"budget": "test", "limit": "100"}]'
449+ self.fake_unexpected_budgets_json = json.loads(
450+ '{"budgets":' + unexpected_snippet + '}')
451+ self.fake_unexpected_budget_json = json.loads(unexpected_snippet)
452+
453+ def test_assess_list_budgets(self):
454+ with patch.object(self.fake_client, 'get_juju_output'):
455+ with patch("assess_budget.json.loads",
456+ return_value=self.fake_budgets_json):
457+ assess_list_budgets(self.fake_client, self.fake_budget_json)
458+
459+ def test_raises_list_mismatch(self):
460+ with patch.object(self.fake_client, 'get_juju_output'):
461+ with patch("assess_budget.json.loads",
462+ return_value=self.fake_unexpected_budgets_json):
463+ with self.assertRaises(JujuAssertionError) as ex:
464+ assess_list_budgets(
465+ self.fake_client, self.fake_budget_json)
466+ self.assertEqual(ex.exception.message,
467+ 'Found: {}\nExpected: {}'.format(
468+ self.fake_unexpected_budget_json,
469+ self.fake_budget_json))
470+
471+
472+class TestAssessSetBudget(TestAssess):
473+
474+ def test_assess_set_budget(self):
475+ with patch.object(self.fake_client, 'get_juju_output'):
476+ with patch("assess_budget.json.loads"):
477+ with patch("assess_budget._try_setting_budget"):
478+ with patch("assess_budget.assert_set_budget"):
479+ assess_set_budget(self.fake_client, self.budget_name,
480+ self.budget_value, self.budget_limit)
481+
482+ def test_raises_on_exceed_credit_limit(self):
483+ with patch.object(self.fake_client, 'get_juju_output'):
484+ with patch("assess_budget.json.loads"):
485+ with patch("assess_budget._try_setting_budget"):
486+ with self.assertRaises(JujuAssertionError) as ex:
487+ _try_greater_than_limit_budget(self.fake_client,
488+ self.budget_name,
489+ self.budget_limit)
490+ self.assertEqual(ex.exception.message,
491+ 'Credit limit exceeded')
492+
493+ def test_raises_on_negative_budget(self):
494+ self.budget_limit = -abs(self.budget_limit)
495+ with patch.object(self.fake_client, 'get_juju_output'):
496+ with patch("assess_budget.json.loads"):
497+ with patch("assess_budget._try_setting_budget"):
498+ with self.assertRaises(JujuAssertionError) as ex:
499+ _try_negative_budget(self.fake_client,
500+ self.budget_name)
501+ self.assertEqual(ex.exception.message,
502+ 'Negative budget allowed')
503+
504+
505+class TestAssessCreateBudget(TestAssess):
506+
507+ def test_assess_create_budget(self):
508+ with patch.object(self.fake_client, 'get_juju_output'):
509+ with patch("assess_budget.create_budget"):
510+ assess_create_budget(self.fake_client, self.budget_name,
511+ self.budget_value, self.budget_limit)
512+
513+ def test_raises_duplicate_budget(self):
514+ with patch.object(self.fake_client, 'get_juju_output'):
515+ with patch("assess_budget.create_budget",
516+ side_effect=JujuAssertionError):
517+ with self.assertRaises(JujuAssertionError) as ex:
518+ assess_create_budget(self.fake_client, self.budget_name,
519+ self.budget_value, self.budget_limit)
520+ self.assertEqual(ex.exception.message,
521+ 'Added duplicate budget')
522+
523+ def test_raises_creation_error(self):
524+ with patch.object(self.fake_client, 'get_juju_output'):
525+ with patch("assess_budget.create_budget",
526+ side_effect=CalledProcessError(1, 'foo', 'bar')):
527+ with self.assertRaises(JujuAssertionError) as ex:
528+ assess_create_budget(self.fake_client, self.budget_name,
529+ self.budget_value, self.budget_limit)
530+ self.assertEqual(ex.exception.message,
531+ "Error testing create-budget: ['bar', '']")
532+
533+
534+class TestAssessBudgetLimit(TestAssess):
535+
536+ def test_assess_budget_limt(self):
537+ budget_limit = randint(1, 10000)
538+ assess_budget_limit(budget_limit)
539+
540+ def test_raises_error_on_negative_limit(self):
541+ neg_budget_limit = randint(-1000, -1)
542+ with self.assertRaises(JujuAssertionError) as ex:
543+ assess_budget_limit(neg_budget_limit)
544+ self.assertEqual(ex.exception.message,
545+ 'Negative Budget Limit {}'.format(neg_budget_limit))

Subscribers

People subscribed via source and target branches