Merge lp:~bcsaller/charm-tools/test-cfg into lp:charm-tools/1.2

Proposed by Benjamin Saller
Status: Rejected
Rejected by: Marco Ceppi
Proposed branch: lp:~bcsaller/charm-tools/test-cfg
Merge into: lp:charm-tools/1.2
Diff against target: 417 lines (+175/-28)
7 files modified
.bzrignore (+2/-0)
charmtools/bundles.py (+0/-1)
charmtools/generate.py (+0/-1)
charmtools/test.py (+98/-17)
charmtools/update.py (+2/-0)
tests/test_juju_test.py (+12/-9)
tests/test_substrates.py (+61/-0)
To merge this branch: bzr merge lp:~bcsaller/charm-tools/test-cfg
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+200959@code.launchpad.net

Description of the change

Substrate filter rules

As a slightly most testable impl of Marco's work I'm offering this as a
counter proposal. Happy to talk through the thought process.

https://codereview.appspot.com/49670043/

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Reviewers: mp+200959_code.launchpad.net,

Message:
Please take a look.

Description:
Substrate filter rules

As a slightly most testable impl of Marco's work I'm offering this as a
counter proposal. Happy to talk through the thought process.

https://code.launchpad.net/~bcsaller/charm-tools/test-cfg/+merge/200959

(do not edit description out of merge proposal)

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

Affected files (+177, -28 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
Marco Ceppi (marcoceppi) wrote :

This is an interesting concept of defining order and making it explicit.
I didn't quite grasp it until I saw the unit tests for it. I'm not
opposed to it now, test.py:652 is still a concern but my comment on 473
now no longer applies

https://codereview.appspot.com/49670043/diff/1/charmtools/test.py
File charmtools/test.py (right):

https://codereview.appspot.com/49670043/diff/1/charmtools/test.py#newcode473
charmtools/test.py:473: 'order should be defined using only include and
skip')
Isn't it counter intuitive to have both include and skip? By design if
you only have an `include` key it'll skip everything not in that include
key. Likewise, if you have a skip it's understood that you'll do
everything but. Seems this should just be OR

https://codereview.appspot.com/49670043/diff/1/charmtools/test.py#newcode652
charmtools/test.py:652: rules = parse_substrates(cfg)
This wouldn't work as cfg is a TestCfg object that only has everything
in the 'options' key. Substrate data would be in 'substrates' at the
same level of 'options'. So if parse_substrates took the test_plan file
and did a parsing that would work. Alternatively TestCfg could (should?)
parse what's in substrates and store it as part of the object so you can
just pass cfg as you're doing here instead of re-parsing the
test_config.yaml file

https://codereview.appspot.com/49670043/

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2013-12-16 16:12:29 +0000
3+++ .bzrignore 2014-01-09 07:49:22 +0000
4@@ -9,3 +9,5 @@
5 lib
6 local
7 man
8+tests/.ropeproject/
9+charmtools/.ropeproject/
10
11=== modified file 'charmtools/bundles.py'
12--- charmtools/bundles.py 2013-12-20 14:06:43 +0000
13+++ charmtools/bundles.py 2014-01-09 07:49:22 +0000
14@@ -2,7 +2,6 @@
15 import yaml
16 import glob
17 import json
18-import requests
19
20 from linter import Linter
21 from charmworldlib import bundle as cw_bundle
22
23=== modified file 'charmtools/generate.py'
24--- charmtools/generate.py 2013-12-12 17:20:39 +0000
25+++ charmtools/generate.py 2014-01-09 07:49:22 +0000
26@@ -16,7 +16,6 @@
27 # along with this program. If not, see <http://www.gnu.org/licenses/>.
28
29 import os
30-import sys
31 import shutil
32 import argparse
33
34
35=== modified file 'charmtools/test.py'
36--- charmtools/test.py 2013-12-21 12:59:08 +0000
37+++ charmtools/test.py 2014-01-09 07:49:22 +0000
38@@ -1,18 +1,16 @@
39 #!/usr/bin/python
40 # coding=latin-1
41
42+import argparse
43+import glob
44+import logging
45 import os
46+import re
47+import signal
48+import subprocess
49 import sys
50-import uuid
51+import time
52 import yaml
53-import time
54-import glob
55-import signal
56-import logging
57-import argparse
58-import subprocess
59-
60-from tempfile import mkdtemp
61 from datetime import timedelta
62 from contextlib import contextmanager
63
64@@ -44,6 +42,10 @@
65 pass
66
67
68+class SubstrateMismatch(Exception):
69+ pass
70+
71+
72 class TimeoutError(Exception):
73 def __init__(self, value="Timed Out"):
74 self.value = value
75@@ -114,7 +116,7 @@
76 self.destroy(self.juju_env)
77 except DestroyUnreliable:
78 self.log.warn('Unable to destroy bootstrap, trying again')
79- sleep(2)
80+ time.sleep(2)
81 try:
82 self.destroy(self.juju_env)
83 except:
84@@ -258,6 +260,7 @@
85 try:
86 with timeout(self.conductor.args.timeout):
87 output = subprocess.check_output(self.test, env=self.env)
88+ self.log.debug(output)
89 except TimeoutError, e:
90 self.log.debug('Killed by timeout after %s seconds',
91 self.conductor.args.timeout)
92@@ -279,7 +282,7 @@
93 try:
94 self.archive_logs()
95 except OrchestraError, e:
96- juju.log.error(e)
97+ self.log.error(e)
98
99 if error:
100 raise error
101@@ -293,7 +296,7 @@
102 raise OrchestraError('Unable to query juju status')
103
104 services = status['services']
105- machines = status['machines']
106+ # machines = status['machines']
107
108 if self.conductor.juju_version.major == 0:
109 logs.append('/var/lib/juju/units/./*/charm.log')
110@@ -402,9 +405,10 @@
111 with open(config_file) as f:
112 cfg = yaml.safe_load(f.read())
113
114- for key, val in cfg['options'].iteritems():
115- if key in self._keys:
116- setattr(self, key, val)
117+ if 'options' in cfg:
118+ for key, val in cfg['options'].iteritems():
119+ if key in self._keys:
120+ setattr(self, key, val)
121
122
123 def get_juju_version():
124@@ -455,6 +459,71 @@
125 return logger
126
127
128+class SubstrateFilter(object):
129+ def __init__(self, spec):
130+ self.order = spec.get('order', ['include', 'skip'])
131+ self.include = spec.get('include', ['*'])
132+ self.skip = spec.get('skip', [])
133+
134+ if isinstance(self.order, str):
135+ self.order = [s.strip() for s in self.order.split(',')]
136+ if self.order != ['include', 'skip'] and \
137+ self.order != ['skip', 'include']:
138+ raise ValueError(
139+ 'order should be defined using only include and skip')
140+
141+ if isinstance(self.include, str):
142+ self.include = [self.include]
143+ self.include = set(self.include)
144+
145+ if isinstance(self.skip, str):
146+ self.skip = [self.skip]
147+ self.skip = set(self.skip)
148+
149+ def filter(self, substrates):
150+ """
151+ Filter a list of substrates relative to the rules generated on class
152+ creation.
153+ """
154+ if isinstance(substrates, str):
155+ substrates = [s.strip() for s in re.split('[,\s]', substrates)]
156+
157+ # Apply the rules to the list of substrates returning anything that
158+ # matches
159+ if self.order == ['include', 'skip']:
160+ result = self._filter_includes(substrates, True)
161+ result = self._filter_skips(result)
162+ else:
163+ result = self._filter_skips(substrates, True)
164+ result = self._filter_includes(result)
165+ return result
166+
167+ def _filter_includes(self, inputList, priority=False):
168+ if priority and '*' in self.include:
169+ return inputList
170+ return sorted(list(set(inputList).intersection(self.include)))
171+
172+ def _filter_skips(self, inputList, priority=False):
173+ if priority and '*' in self.skip:
174+ return list(self.include.intersection(inputList))
175+ return sorted(list(set(inputList).difference(self.skip)))
176+
177+
178+def parse_substrates(spec):
179+ if isinstance(spec, basestring):
180+ spec = yaml.safe_load(spec)
181+ if not spec or 'substrates' not in spec:
182+ raise ValueError(
183+ "Invalid data passed to parse_substrates: {}".format(spec))
184+
185+ specRules = SubstrateFilter(spec['substrates'])
186+ return specRules
187+
188+
189+def allowed_substrates(spec, possible_substrates):
190+ return parse_substrates(spec).filter(possible_substrates)
191+
192+
193 def setup_parser():
194 parser = argparse.ArgumentParser(
195 prog='juju test',
196@@ -564,8 +633,9 @@
197 logger.info('Starting test run on %s using Juju %s'
198 % (args.juju_env, get_juju_version()))
199 logger.debug('Loading configuration options from testplan YAML')
200- test_plans = glob.glob(os.path.join(os.getcwd(), 'tests', 'testplan.y*ml'))
201- test_plan = test_plan[0] if test_plans else None
202+ test_plans = glob.glob(os.path.join(os.getcwd(), 'tests',
203+ 'test_config.y*ml'))
204+ test_plan = test_plans[0] if test_plans else None
205 if test_plan:
206 cfg = TestCfg(test_plan)
207 for key, val in args.iteritems():
208@@ -577,10 +647,21 @@
209 logger.debug('Creating a new Conductor')
210 try:
211 tester = Conductor(args)
212+ env_yaml = tester.get_environment(cfg.juju_env)
213+ if 'substrates' in cfg:
214+ rules = parse_substrates(cfg)
215+ allowed = rules.filter(env_yaml['type'])
216+ if env_yaml['type'] not in allowed:
217+ raise Exception('%s is not an allowed substrate: %s' %
218+ (env_yaml['type'],
219+ allowed.join(', ')))
220 errors, failures, passes = tester.run()
221 except NoTests:
222 logger.critical('No tests were found')
223 sys.exit(1)
224+ except Exception as e:
225+ logger.critical(str(e))
226+ sys.exit(1)
227 except:
228 raise
229
230
231=== modified file 'charmtools/update.py'
232--- charmtools/update.py 2013-12-16 16:12:29 +0000
233+++ charmtools/update.py 2014-01-09 07:49:22 +0000
234@@ -34,6 +34,7 @@
235
236 return parser
237
238+
239 def update(charm_dir, fix=False):
240 mr = Mr(charm_dir)
241 for charm in charms.remote():
242@@ -48,6 +49,7 @@
243 except Exception as e:
244 raise Exception(".mrconfig not saved: %s" % e.strerror)
245
246+
247 def main():
248 parser = setup_parser()
249 args = parser.parse_args()
250
251=== modified file 'tests/test_juju_test.py'
252--- tests/test_juju_test.py 2013-12-20 12:33:21 +0000
253+++ tests/test_juju_test.py 2014-01-09 07:49:22 +0000
254@@ -30,13 +30,16 @@
255
256 PARSED_ENVIRONMENTS_YAML = yaml.safe_load(RAW_ENVIRONMENTS_YAML)
257
258+
259 class Arguments(object):
260 def __init__(self, **kwargs):
261 for key, value in kwargs.items():
262 setattr(self, key, value)
263+
264 def __getattr__(self, name):
265 return None
266
267+
268 class JujuTestPluginTest(unittest.TestCase):
269 @patch('subprocess.check_output')
270 def test_get_gojuju_version(self, mock_check_output):
271@@ -77,7 +80,7 @@
272 self.assertEqual(100, juju_test.convert_to_timedelta('100s'))
273 self.assertEqual(100, juju_test.convert_to_timedelta(100))
274 self.assertEqual(60, juju_test.convert_to_timedelta('1m'))
275- self.assertEqual(60*60, juju_test.convert_to_timedelta('1h'))
276+ self.assertEqual(60 * 60, juju_test.convert_to_timedelta('1h'))
277
278 @patch('glob.glob')
279 @patch('os.path.isfile')
280@@ -89,7 +92,7 @@
281 mock_isfile.side_effect = files_exist
282 mock_glob.return_value = tests_directory
283
284- args = Arguments(tests = 'dummy')
285+ args = Arguments(tests='dummy')
286 c = juju_test.Conductor(args)
287 results = c.find_tests()
288
289@@ -100,7 +103,7 @@
290 def test_conductor_find_tests_fails(self, mock_glob):
291 mock_glob.return_value = []
292
293- args = Arguments(tests = 'dummy')
294+ args = Arguments(tests='dummy')
295 c = juju_test.Conductor(args)
296 results = c.find_tests()
297
298@@ -126,7 +129,7 @@
299
300 mcheck_output.side_effect = [yml_output, Exception('not bootstrapped')]
301 juju_env = 'test'
302- args = Arguments(tests = 'dummy')
303+ args = Arguments(tests='dummy')
304 c = juju_test.Conductor(args)
305 results = c.status(juju_env)
306
307@@ -193,7 +196,7 @@
308
309 @patch('subprocess.check_call')
310 def test_conductor_destroy(self, mock_check_call):
311- args = Arguments(tests = 'dummy')
312+ args = Arguments(tests='dummy')
313 c = juju_test.Conductor(args)
314 c.juju_version = juju_test.JujuVersion(major=1, minor=1, patch=1)
315 good_env = 'valid'
316@@ -216,7 +219,7 @@
317
318 bad_env = 'invalid'
319
320- args = Arguments(tests = 'dummy')
321+ args = Arguments(tests='dummy')
322 c = juju_test.Conductor(args)
323 c.juju_version = juju_test.JujuVersion(major=1, minor=8, patch=0)
324 self.assertRaises(juju_test.DestroyUnreliable, c.destroy, bad_env)
325@@ -448,7 +451,7 @@
326 o.log.status.assert_called_with('%s (%s)' % (juju_test.TEST_FAIL,
327 juju_test.TEST_TIMEOUT))
328
329- o.conductor.args.on_timeout='skip'
330+ o.conductor.args.on_timeout = 'skip'
331 o.log.status.reset_mock()
332 o.print_status(124)
333 o.log.status.assert_called_with('%s (%s)' % (juju_test.TEST_FAIL,
334@@ -583,7 +586,7 @@
335 goyml_parsed = yaml.safe_load(goyml_output)
336 mstatus.return_value = goyml_parsed
337
338- juju_env='testing'
339+ juju_env = 'testing'
340 args = Arguments(tests='dummy', juju_env=juju_env, logdir='/tmp')
341 c = juju_test.Conductor(args)
342 c.juju_version = juju_test.JujuVersion(major=1, minor=10, patch=0)
343@@ -623,7 +626,7 @@
344 pyyml_parsed = yaml.safe_load(pyyml_output)
345 mstatus.return_value = pyyml_parsed
346
347- juju_env='testing'
348+ juju_env = 'testing'
349 args = Arguments(tests='dummy', juju_env=juju_env, logdir='/tmp')
350 c = juju_test.Conductor(args)
351 c.juju_version = juju_test.JujuVersion(major=0, minor=7, patch=0)
352
353=== added file 'tests/test_substrates.py'
354--- tests/test_substrates.py 1970-01-01 00:00:00 +0000
355+++ tests/test_substrates.py 2014-01-09 07:49:22 +0000
356@@ -0,0 +1,61 @@
357+"""Unit test for juju tests substrate handling"""
358+
359+import unittest
360+import yaml
361+from charmtools.test import parse_substrates, allowed_substrates
362+
363+class JujuTestSubstrateTests(unittest.TestCase):
364+ INCLUDE_SKIP = '''
365+substrates:
366+ order: include, skip
367+ skip: amazon
368+ include: "*"
369+ '''
370+
371+ SKIP_INCLUDE = '''
372+substrates:
373+ order: skip, include
374+ skip: "*"
375+ include: amazon
376+ '''
377+
378+ def test_parse_substrates_text(self):
379+ self.assertRaises(ValueError, parse_substrates, '')
380+ self.assertIsNotNone(parse_substrates(self.INCLUDE_SKIP))
381+
382+ def test_parse_substrate_object(self):
383+ data = yaml.load(self.INCLUDE_SKIP)
384+ self.assertIsNotNone(parse_substrates(data))
385+
386+ def test_parse_substrates_order(self):
387+ result = parse_substrates(self.INCLUDE_SKIP)
388+ self.assertEqual(result.order, ['include', 'skip'])
389+
390+ def test_parse_substrates_order_invalid(self):
391+ data = self.INCLUDE_SKIP.replace('skip', 'foobar')
392+ self.assertRaises(ValueError, parse_substrates, data)
393+
394+ def test_parse_substrates_include(self):
395+ result = parse_substrates(self.INCLUDE_SKIP)
396+ self.assertEqual(result.include, set(['*']))
397+
398+ def test_parse_substrates_skip(self):
399+ result = parse_substrates(self.INCLUDE_SKIP)
400+ self.assertEqual(result.skip, set(['amazon']))
401+
402+ def test_filter_include_skip(self):
403+ rules = parse_substrates(self.INCLUDE_SKIP)
404+ self.assertEqual(rules.filter('amazon,hp,azure'), ['azure', 'hp'])
405+
406+ def test_filter_skip_include(self):
407+ rules = parse_substrates(self.SKIP_INCLUDE)
408+ self.assertEqual(rules.filter('amazon,hp,azure'), ['amazon'])
409+
410+ def test_allowed_substrates(self):
411+ self.assertEqual(allowed_substrates(self.INCLUDE_SKIP,
412+ ['foo', 'bar', 'amazon']),
413+ ['bar', 'foo'])
414+
415+
416+if __name__ == '__main__':
417+ unittest.main()

Subscribers

People subscribed via source and target branches

to all changes: