Merge lp:~bcsaller/charm-tools/test-cfg into lp:charm-tools/1.2
- test-cfg
- Merge into 1.2
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email: mp+200959@code.launchpad.net |
Commit message
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.
Benjamin Saller (bcsaller) wrote : | # |
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:/
File charmtools/test.py (right):
https:/
charmtools/
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:/
charmtools/
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
Unmerged revisions
Preview Diff
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() |
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): bundles. py generate. py update. py juju_test. py substrates. py
M .bzrignore
A [revision details]
M charmtools/
M charmtools/
M charmtools/test.py
M charmtools/
M tests/test_
A tests/test_