Merge lp:~marcoceppi/charm-tools/substrate-cfg into lp:charm-tools/1.2

Proposed by Marco Ceppi
Status: Merged
Merged at revision: 287
Proposed branch: lp:~marcoceppi/charm-tools/substrate-cfg
Merge into: lp:charm-tools/1.2
Diff against target: 438 lines (+187/-37)
7 files modified
.bzrignore (+2/-0)
charmtools/bundles.py (+0/-1)
charmtools/generate.py (+0/-1)
charmtools/test.py (+109/-26)
charmtools/update.py (+2/-0)
tests/test_juju_test.py (+12/-9)
tests/test_substrates.py (+62/-0)
To merge this branch: bzr merge lp:~marcoceppi/charm-tools/substrate-cfg
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+201094@code.launchpad.net

Description of the change

Add substrate configuration to juju-test

https://codereview.appspot.com/49940044/

To post a comment you must log in.
Revision history for this message
Marco Ceppi (marcoceppi) wrote :

Reviewers: mp+201094_code.launchpad.net,

Message:
Please take a look.

Description:
Updates to bsaller's fixes to substrate parsing

https://code.launchpad.net/~marcoceppi/charm-tools/substrate-cfg/+merge/201094

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/49940044/

Affected files (+189, -37 lines):
   M .bzrignore
   A [revision details]
   M charmtools/bundles.py
   M charmtools/generate.py
   M charmtools/test.py
   M charmtools/update.py
   M tests/test_juju_test.py
   A tests/test_substrates.py

Revision history for this message
Benjamin Saller (bcsaller) wrote :

LGTM

Thanks for including my suggestions (and making the actual integration
work). The changes to using a key whitelist in TestCfg are also good.

https://codereview.appspot.com/49940044/

Revision history for this message
Marco Ceppi (marcoceppi) wrote :

*** Submitted:

Add substrate configuration to juju-test

R=benjamin.saller
CC=
https://codereview.appspot.com/49940044

https://codereview.appspot.com/49940044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2013-12-16 16:12:29 +0000
+++ .bzrignore 2014-01-09 20:17:53 +0000
@@ -9,3 +9,5 @@
9lib9lib
10local10local
11man11man
12tests/.ropeproject/
13charmtools/.ropeproject/
1214
=== modified file 'charmtools/bundles.py'
--- charmtools/bundles.py 2013-12-20 14:06:43 +0000
+++ charmtools/bundles.py 2014-01-09 20:17:53 +0000
@@ -2,7 +2,6 @@
2import yaml2import yaml
3import glob3import glob
4import json4import json
5import requests
65
7from linter import Linter6from linter import Linter
8from charmworldlib import bundle as cw_bundle7from charmworldlib import bundle as cw_bundle
98
=== modified file 'charmtools/generate.py'
--- charmtools/generate.py 2013-12-12 17:20:39 +0000
+++ charmtools/generate.py 2014-01-09 20:17:53 +0000
@@ -16,7 +16,6 @@
16# along with this program. If not, see <http://www.gnu.org/licenses/>.16# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
18import os18import os
19import sys
20import shutil19import shutil
21import argparse20import argparse
2221
2322
=== modified file 'charmtools/test.py'
--- charmtools/test.py 2013-12-21 12:59:08 +0000
+++ charmtools/test.py 2014-01-09 20:17:53 +0000
@@ -1,18 +1,16 @@
1#!/usr/bin/python1#!/usr/bin/python
2# coding=latin-12# coding=latin-1
33
4import argparse
5import glob
6import logging
4import os7import os
8import re
9import signal
10import subprocess
5import sys11import sys
6import uuid12import time
7import yaml13import yaml
8import time
9import glob
10import signal
11import logging
12import argparse
13import subprocess
14
15from tempfile import mkdtemp
16from datetime import timedelta14from datetime import timedelta
17from contextlib import contextmanager15from contextlib import contextmanager
1816
@@ -44,6 +42,10 @@
44 pass42 pass
4543
4644
45class SubstrateMismatch(Exception):
46 pass
47
48
47class TimeoutError(Exception):49class TimeoutError(Exception):
48 def __init__(self, value="Timed Out"):50 def __init__(self, value="Timed Out"):
49 self.value = value51 self.value = value
@@ -114,7 +116,7 @@
114 self.destroy(self.juju_env)116 self.destroy(self.juju_env)
115 except DestroyUnreliable:117 except DestroyUnreliable:
116 self.log.warn('Unable to destroy bootstrap, trying again')118 self.log.warn('Unable to destroy bootstrap, trying again')
117 sleep(2)119 time.sleep(2)
118 try:120 try:
119 self.destroy(self.juju_env)121 self.destroy(self.juju_env)
120 except:122 except:
@@ -258,6 +260,7 @@
258 try:260 try:
259 with timeout(self.conductor.args.timeout):261 with timeout(self.conductor.args.timeout):
260 output = subprocess.check_output(self.test, env=self.env)262 output = subprocess.check_output(self.test, env=self.env)
263 self.log.debug(output)
261 except TimeoutError, e:264 except TimeoutError, e:
262 self.log.debug('Killed by timeout after %s seconds',265 self.log.debug('Killed by timeout after %s seconds',
263 self.conductor.args.timeout)266 self.conductor.args.timeout)
@@ -279,7 +282,7 @@
279 try:282 try:
280 self.archive_logs()283 self.archive_logs()
281 except OrchestraError, e:284 except OrchestraError, e:
282 juju.log.error(e)285 self.log.error(e)
283286
284 if error:287 if error:
285 raise error288 raise error
@@ -293,7 +296,7 @@
293 raise OrchestraError('Unable to query juju status')296 raise OrchestraError('Unable to query juju status')
294297
295 services = status['services']298 services = status['services']
296 machines = status['machines']299 # machines = status['machines']
297300
298 if self.conductor.juju_version.major == 0:301 if self.conductor.juju_version.major == 0:
299 logs.append('/var/lib/juju/units/./*/charm.log')302 logs.append('/var/lib/juju/units/./*/charm.log')
@@ -395,16 +398,16 @@
395class TestCfg(object):398class TestCfg(object):
396 _keys = ['timeout', 'set-e', 'on-timeout', 'fail-on-skip', 'tests']399 _keys = ['timeout', 'set-e', 'on-timeout', 'fail-on-skip', 'tests']
397400
398 def __init__(self, config_file):401 def __init__(self, cfg):
399 if not os.path.exists(config_file):402 if isinstance(cfg, basestring):
400 raise OSError("%s not found" % config_file)403 cfg = yaml.safe_load(cfg)
401404
402 with open(config_file) as f:405 if 'options' in cfg:
403 cfg = yaml.safe_load(f.read())406 for key, val in cfg['options'].iteritems():
404407 if key in self._keys:
405 for key, val in cfg['options'].iteritems():408 setattr(self, key, val)
406 if key in self._keys:409 if 'substrates' in cfg:
407 setattr(self, key, val)410 self.substrates = cfg.substrates
408411
409412
410def get_juju_version():413def get_juju_version():
@@ -455,6 +458,71 @@
455 return logger458 return logger
456459
457460
461class SubstrateFilter(object):
462 def __init__(self, spec):
463 self.order = spec.get('order', ['include', 'skip'])
464 self.include = spec.get('include', ['*'])
465 self.skip = spec.get('skip', [])
466
467 if isinstance(self.order, str):
468 self.order = [s.strip() for s in self.order.split(',')]
469 if self.order != ['include', 'skip'] and \
470 self.order != ['skip', 'include']:
471 raise ValueError(
472 'order should be defined using only include and skip')
473
474 if isinstance(self.include, str):
475 self.include = [self.include]
476 self.include = set(self.include)
477
478 if isinstance(self.skip, str):
479 self.skip = [self.skip]
480 self.skip = set(self.skip)
481
482 def filter(self, substrates):
483 """
484 Filter a list of substrates relative to the rules generated on class
485 creation.
486 """
487 if isinstance(substrates, str):
488 substrates = [s.strip() for s in re.split('[,\s]', substrates)]
489
490 # Apply the rules to the list of substrates returning anything that
491 # matches
492 if self.order == ['include', 'skip']:
493 result = self._filter_includes(substrates, True)
494 result = self._filter_skips(result)
495 else:
496 result = self._filter_skips(substrates, True)
497 result = self._filter_includes(result)
498 return result
499
500 def _filter_includes(self, inputList, priority=False):
501 if priority and '*' in self.include:
502 return inputList
503 return sorted(list(set(inputList).intersection(self.include)))
504
505 def _filter_skips(self, inputList, priority=False):
506 if priority and '*' in self.skip:
507 return list(self.include.intersection(inputList))
508 return sorted(list(set(inputList).difference(self.skip)))
509
510
511def parse_substrates(spec):
512 if isinstance(spec, basestring):
513 spec = yaml.safe_load(spec)
514 if not spec or 'substrates' not in spec:
515 raise ValueError(
516 "Invalid data passed to parse_substrates: {}".format(spec))
517
518 specRules = SubstrateFilter(spec['substrates'])
519 return specRules
520
521
522def allowed_substrates(spec, possible_substrates):
523 return parse_substrates(spec).filter(possible_substrates)
524
525
458def setup_parser():526def setup_parser():
459 parser = argparse.ArgumentParser(527 parser = argparse.ArgumentParser(
460 prog='juju test',528 prog='juju test',
@@ -564,10 +632,14 @@
564 logger.info('Starting test run on %s using Juju %s'632 logger.info('Starting test run on %s using Juju %s'
565 % (args.juju_env, get_juju_version()))633 % (args.juju_env, get_juju_version()))
566 logger.debug('Loading configuration options from testplan YAML')634 logger.debug('Loading configuration options from testplan YAML')
567 test_plans = glob.glob(os.path.join(os.getcwd(), 'tests', 'testplan.y*ml'))635 test_plans = glob.glob(os.path.join(os.getcwd(), 'tests',
568 test_plan = test_plan[0] if test_plans else None636 'test_config.y*ml'))
569 if test_plan:637 if test_plans:
570 cfg = TestCfg(test_plan)638 with open(test_plans[0]) as f:
639 test_cfg = f.read()
640
641 if test_cfg:
642 cfg = TestCfg(test_cfg)
571 for key, val in args.iteritems():643 for key, val in args.iteritems():
572 logger.debug('Overwriting %s to %s from cmd' % (key, val))644 logger.debug('Overwriting %s to %s from cmd' % (key, val))
573 setattr(cfg, key, val)645 setattr(cfg, key, val)
@@ -577,10 +649,21 @@
577 logger.debug('Creating a new Conductor')649 logger.debug('Creating a new Conductor')
578 try:650 try:
579 tester = Conductor(args)651 tester = Conductor(args)
652 env_yaml = tester.get_environment(cfg.juju_env)
653 if 'substrates' in cfg:
654 rules = parse_substrates(cfg)
655 allowed = rules.filter(env_yaml['type'])
656 if env_yaml['type'] not in allowed:
657 raise Exception('%s is not an allowed substrate: %s' %
658 (env_yaml['type'],
659 allowed.join(', ')))
580 errors, failures, passes = tester.run()660 errors, failures, passes = tester.run()
581 except NoTests:661 except NoTests:
582 logger.critical('No tests were found')662 logger.critical('No tests were found')
583 sys.exit(1)663 sys.exit(1)
664 except Exception as e:
665 logger.critical(str(e))
666 sys.exit(1)
584 except:667 except:
585 raise668 raise
586669
587670
=== modified file 'charmtools/update.py'
--- charmtools/update.py 2013-12-16 16:12:29 +0000
+++ charmtools/update.py 2014-01-09 20:17:53 +0000
@@ -34,6 +34,7 @@
3434
35 return parser35 return parser
3636
37
37def update(charm_dir, fix=False):38def update(charm_dir, fix=False):
38 mr = Mr(charm_dir)39 mr = Mr(charm_dir)
39 for charm in charms.remote():40 for charm in charms.remote():
@@ -48,6 +49,7 @@
48 except Exception as e:49 except Exception as e:
49 raise Exception(".mrconfig not saved: %s" % e.strerror)50 raise Exception(".mrconfig not saved: %s" % e.strerror)
5051
52
51def main():53def main():
52 parser = setup_parser()54 parser = setup_parser()
53 args = parser.parse_args()55 args = parser.parse_args()
5456
=== modified file 'tests/test_juju_test.py'
--- tests/test_juju_test.py 2013-12-20 12:33:21 +0000
+++ tests/test_juju_test.py 2014-01-09 20:17:53 +0000
@@ -30,13 +30,16 @@
3030
31PARSED_ENVIRONMENTS_YAML = yaml.safe_load(RAW_ENVIRONMENTS_YAML)31PARSED_ENVIRONMENTS_YAML = yaml.safe_load(RAW_ENVIRONMENTS_YAML)
3232
33
33class Arguments(object):34class Arguments(object):
34 def __init__(self, **kwargs):35 def __init__(self, **kwargs):
35 for key, value in kwargs.items():36 for key, value in kwargs.items():
36 setattr(self, key, value)37 setattr(self, key, value)
38
37 def __getattr__(self, name):39 def __getattr__(self, name):
38 return None40 return None
3941
42
40class JujuTestPluginTest(unittest.TestCase):43class JujuTestPluginTest(unittest.TestCase):
41 @patch('subprocess.check_output')44 @patch('subprocess.check_output')
42 def test_get_gojuju_version(self, mock_check_output):45 def test_get_gojuju_version(self, mock_check_output):
@@ -77,7 +80,7 @@
77 self.assertEqual(100, juju_test.convert_to_timedelta('100s'))80 self.assertEqual(100, juju_test.convert_to_timedelta('100s'))
78 self.assertEqual(100, juju_test.convert_to_timedelta(100))81 self.assertEqual(100, juju_test.convert_to_timedelta(100))
79 self.assertEqual(60, juju_test.convert_to_timedelta('1m'))82 self.assertEqual(60, juju_test.convert_to_timedelta('1m'))
80 self.assertEqual(60*60, juju_test.convert_to_timedelta('1h'))83 self.assertEqual(60 * 60, juju_test.convert_to_timedelta('1h'))
8184
82 @patch('glob.glob')85 @patch('glob.glob')
83 @patch('os.path.isfile')86 @patch('os.path.isfile')
@@ -89,7 +92,7 @@
89 mock_isfile.side_effect = files_exist92 mock_isfile.side_effect = files_exist
90 mock_glob.return_value = tests_directory93 mock_glob.return_value = tests_directory
9194
92 args = Arguments(tests = 'dummy')95 args = Arguments(tests='dummy')
93 c = juju_test.Conductor(args)96 c = juju_test.Conductor(args)
94 results = c.find_tests()97 results = c.find_tests()
9598
@@ -100,7 +103,7 @@
100 def test_conductor_find_tests_fails(self, mock_glob):103 def test_conductor_find_tests_fails(self, mock_glob):
101 mock_glob.return_value = []104 mock_glob.return_value = []
102105
103 args = Arguments(tests = 'dummy')106 args = Arguments(tests='dummy')
104 c = juju_test.Conductor(args)107 c = juju_test.Conductor(args)
105 results = c.find_tests()108 results = c.find_tests()
106109
@@ -126,7 +129,7 @@
126129
127 mcheck_output.side_effect = [yml_output, Exception('not bootstrapped')]130 mcheck_output.side_effect = [yml_output, Exception('not bootstrapped')]
128 juju_env = 'test'131 juju_env = 'test'
129 args = Arguments(tests = 'dummy')132 args = Arguments(tests='dummy')
130 c = juju_test.Conductor(args)133 c = juju_test.Conductor(args)
131 results = c.status(juju_env)134 results = c.status(juju_env)
132135
@@ -193,7 +196,7 @@
193196
194 @patch('subprocess.check_call')197 @patch('subprocess.check_call')
195 def test_conductor_destroy(self, mock_check_call):198 def test_conductor_destroy(self, mock_check_call):
196 args = Arguments(tests = 'dummy')199 args = Arguments(tests='dummy')
197 c = juju_test.Conductor(args)200 c = juju_test.Conductor(args)
198 c.juju_version = juju_test.JujuVersion(major=1, minor=1, patch=1)201 c.juju_version = juju_test.JujuVersion(major=1, minor=1, patch=1)
199 good_env = 'valid'202 good_env = 'valid'
@@ -216,7 +219,7 @@
216219
217 bad_env = 'invalid'220 bad_env = 'invalid'
218221
219 args = Arguments(tests = 'dummy')222 args = Arguments(tests='dummy')
220 c = juju_test.Conductor(args)223 c = juju_test.Conductor(args)
221 c.juju_version = juju_test.JujuVersion(major=1, minor=8, patch=0)224 c.juju_version = juju_test.JujuVersion(major=1, minor=8, patch=0)
222 self.assertRaises(juju_test.DestroyUnreliable, c.destroy, bad_env)225 self.assertRaises(juju_test.DestroyUnreliable, c.destroy, bad_env)
@@ -448,7 +451,7 @@
448 o.log.status.assert_called_with('%s (%s)' % (juju_test.TEST_FAIL,451 o.log.status.assert_called_with('%s (%s)' % (juju_test.TEST_FAIL,
449 juju_test.TEST_TIMEOUT))452 juju_test.TEST_TIMEOUT))
450453
451 o.conductor.args.on_timeout='skip'454 o.conductor.args.on_timeout = 'skip'
452 o.log.status.reset_mock()455 o.log.status.reset_mock()
453 o.print_status(124)456 o.print_status(124)
454 o.log.status.assert_called_with('%s (%s)' % (juju_test.TEST_FAIL,457 o.log.status.assert_called_with('%s (%s)' % (juju_test.TEST_FAIL,
@@ -583,7 +586,7 @@
583 goyml_parsed = yaml.safe_load(goyml_output)586 goyml_parsed = yaml.safe_load(goyml_output)
584 mstatus.return_value = goyml_parsed587 mstatus.return_value = goyml_parsed
585588
586 juju_env='testing'589 juju_env = 'testing'
587 args = Arguments(tests='dummy', juju_env=juju_env, logdir='/tmp')590 args = Arguments(tests='dummy', juju_env=juju_env, logdir='/tmp')
588 c = juju_test.Conductor(args)591 c = juju_test.Conductor(args)
589 c.juju_version = juju_test.JujuVersion(major=1, minor=10, patch=0)592 c.juju_version = juju_test.JujuVersion(major=1, minor=10, patch=0)
@@ -623,7 +626,7 @@
623 pyyml_parsed = yaml.safe_load(pyyml_output)626 pyyml_parsed = yaml.safe_load(pyyml_output)
624 mstatus.return_value = pyyml_parsed627 mstatus.return_value = pyyml_parsed
625628
626 juju_env='testing'629 juju_env = 'testing'
627 args = Arguments(tests='dummy', juju_env=juju_env, logdir='/tmp')630 args = Arguments(tests='dummy', juju_env=juju_env, logdir='/tmp')
628 c = juju_test.Conductor(args)631 c = juju_test.Conductor(args)
629 c.juju_version = juju_test.JujuVersion(major=0, minor=7, patch=0)632 c.juju_version = juju_test.JujuVersion(major=0, minor=7, patch=0)
630633
=== added file 'tests/test_substrates.py'
--- tests/test_substrates.py 1970-01-01 00:00:00 +0000
+++ tests/test_substrates.py 2014-01-09 20:17:53 +0000
@@ -0,0 +1,62 @@
1"""Unit test for juju tests substrate handling"""
2
3import unittest
4import yaml
5from charmtools.test import parse_substrates, allowed_substrates
6
7
8class JujuTestSubstrateTests(unittest.TestCase):
9 INCLUDE_SKIP = '''
10substrates:
11 order: include, skip
12 skip: amazon
13 include: "*"
14 '''
15
16 SKIP_INCLUDE = '''
17substrates:
18 order: skip, include
19 skip: "*"
20 include: amazon
21 '''
22
23 def test_parse_substrates_text(self):
24 self.assertRaises(ValueError, parse_substrates, '')
25 self.assertIsNotNone(parse_substrates(self.INCLUDE_SKIP))
26
27 def test_parse_substrate_object(self):
28 data = yaml.load(self.INCLUDE_SKIP)
29 self.assertIsNotNone(parse_substrates(data))
30
31 def test_parse_substrates_order(self):
32 result = parse_substrates(self.INCLUDE_SKIP)
33 self.assertEqual(result.order, ['include', 'skip'])
34
35 def test_parse_substrates_order_invalid(self):
36 data = self.INCLUDE_SKIP.replace('skip', 'foobar')
37 self.assertRaises(ValueError, parse_substrates, data)
38
39 def test_parse_substrates_include(self):
40 result = parse_substrates(self.INCLUDE_SKIP)
41 self.assertEqual(result.include, set(['*']))
42
43 def test_parse_substrates_skip(self):
44 result = parse_substrates(self.INCLUDE_SKIP)
45 self.assertEqual(result.skip, set(['amazon']))
46
47 def test_filter_include_skip(self):
48 rules = parse_substrates(self.INCLUDE_SKIP)
49 self.assertEqual(rules.filter('amazon,hp,azure'), ['azure', 'hp'])
50
51 def test_filter_skip_include(self):
52 rules = parse_substrates(self.SKIP_INCLUDE)
53 self.assertEqual(rules.filter('amazon,hp,azure'), ['amazon'])
54
55 def test_allowed_substrates(self):
56 self.assertEqual(allowed_substrates(self.INCLUDE_SKIP,
57 ['foo', 'bar', 'amazon']),
58 ['bar', 'foo'])
59
60
61if __name__ == '__main__':
62 unittest.main()

Subscribers

People subscribed via source and target branches

to all changes: