Merge lp:~doanac/utah/process_runlist_cleanup into lp:utah
- process_runlist_cleanup
- Merge into dev
Status: | Superseded |
---|---|
Proposed branch: | lp:~doanac/utah/process_runlist_cleanup |
Merge into: | lp:utah |
Prerequisite: | lp:~doanac/utah/bzr-retry |
Diff against target: |
919 lines (+634/-122) (has conflicts) 10 files modified
templates/utah-autorun.sh.jinja2 (+41/-0) tests/test_autorun.py (+71/-0) tests/test_config.py (+105/-0) tests/test_provision_data.py (+92/-0) utah/client/runner.py (+60/-54) utah/client/tests/test_client.py (+71/-0) utah/client/tests/test_runner.py (+14/-8) utah/client/testsuite.py (+52/-60) utah/experimental.py (+48/-0) utah/provisioning/data.py (+80/-0) Conflict adding file templates/utah-autorun.sh.jinja2. Moved existing file to templates/utah-autorun.sh.jinja2.moved. Conflict adding file tests/test_autorun.py. Moved existing file to tests/test_autorun.py.moved. Conflict adding file tests/test_config.py. Moved existing file to tests/test_config.py.moved. Conflict adding file tests/test_provision_data.py. Moved existing file to tests/test_provision_data.py.moved. Text conflict in utah/client/runner.py Conflict adding file utah/client/tests/test_client.py. Moved existing file to utah/client/tests/test_client.py.moved. Conflict adding file utah/experimental.py. Moved existing file to utah/experimental.py.moved. Conflict adding file utah/provisioning/data.py. Moved existing file to utah/provisioning/data.py.moved. |
To merge this branch: | bzr merge lp:~doanac/utah/process_runlist_cleanup |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andy Doan | Pending | ||
Review via email:
|
This proposal supersedes a proposal from 2013-06-02.
This proposal has been superseded by a proposal from 2013-06-07.
Commit message
Description of the change
This finishes the cleanup of the overly complex process_
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
- 930. By Max Brustkern
-
Merged DefaultValidator removal to fix the build
- 931. By Andy Doan
-
fix #1185584 retry for bzr failure
- 932. By Andy Doan
-
more simplifications to the process_
master_ runlist function Move logic of getting and validating runlist into its own function.
While fixing I noticed that ContentTooShort
Error is a subclass of
IOError, so the exception logic for that was un-reachable. This makes
one exception case and tries to include useful error information.Also while fixing, I noticed the "suites" logic is no longer needed,
because we only support one type of runlist in the schema we validate
against.This takes us from of mccabe complexity of 16 to 10.
- 933. By Andy Doan
-
break out more of process_
master_ runlist This moves the suite creation logic into its own function
complexity now down to an 8
- 934. By Andy Doan
-
simplify testsuite constructor: break out "add_test" logic
Unmerged revisions
Preview Diff
1 | === added file 'templates/utah-autorun.sh.jinja2' |
2 | --- templates/utah-autorun.sh.jinja2 1970-01-01 00:00:00 +0000 |
3 | +++ templates/utah-autorun.sh.jinja2 2013-06-07 18:58:24 +0000 |
4 | @@ -0,0 +1,41 @@ |
5 | +#!/bin/sh |
6 | + |
7 | +set -e |
8 | + |
9 | +# looks under /etc/utah/autorun for scripts to execute at startup |
10 | +# as scripts are executed they'll be moved to |
11 | +# /var/cache/utah/autorun/inprogress and then to |
12 | +# /var/cache/utah/autorun/complete/. |
13 | + |
14 | +WORKDIR="${WORKDIR-/etc/utah/autorun}" |
15 | +RESULTDIR="${RESULTDIR-/var/cache/utah/autorun}" |
16 | +INPROGRESS=${RESULTDIR}/inprogress |
17 | +COMPLETE=${RESULTDIR}/complete |
18 | + |
19 | +{% include "log-function.jinja2" %} |
20 | + |
21 | +if [ ! -d ${WORKDIR} ] ; then |
22 | + log "No work found under ${WORKDIR}" |
23 | + exit 0 |
24 | +fi |
25 | + |
26 | +[ -d ${INPROGRESS} ] || mkdir -p ${INPROGRESS} |
27 | +[ -d ${COMPLETE} ] || mkdir -p ${COMPLETE} |
28 | + |
29 | +for p in ${WORKDIR}/* ; do |
30 | + if test -f $p && test -x $p ; then |
31 | + log "running $p" |
32 | + fname=$(basename $p) |
33 | + mv $p ${INPROGRESS}/ |
34 | + set +e |
35 | + ${INPROGRESS}/$fname |
36 | + rc=$? |
37 | + set -e |
38 | + mv ${INPROGRESS}/$fname ${COMPLETE}/${fname}.$(date +%s).$rc |
39 | + log "completed $p rc=$rc" |
40 | + if [ $rc -ne 0 ] ; then |
41 | + log "exiting autorun due to failure" |
42 | + exit 1 |
43 | + fi |
44 | + fi |
45 | +done |
46 | |
47 | === renamed file 'templates/utah-autorun.sh.jinja2' => 'templates/utah-autorun.sh.jinja2.moved' |
48 | === added file 'tests/test_autorun.py' |
49 | --- tests/test_autorun.py 1970-01-01 00:00:00 +0000 |
50 | +++ tests/test_autorun.py 2013-06-07 18:58:24 +0000 |
51 | @@ -0,0 +1,71 @@ |
52 | +# Ubuntu Testing Automation Harness |
53 | +# Copyright 2013 Canonical Ltd. |
54 | + |
55 | +# This program is free software: you can redistribute it and/or modify it |
56 | +# under the terms of the GNU General Public License version 3, as published |
57 | +# by the Free Software Foundation. |
58 | + |
59 | +# This program is distributed in the hope that it will be useful, but |
60 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
61 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
62 | +# PURPOSE. See the GNU General Public License for more details. |
63 | + |
64 | +# You should have received a copy of the GNU General Public License along |
65 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
66 | + |
67 | +"""Unit tests for the utah-autorun.sh script.""" |
68 | + |
69 | +import os |
70 | +import re |
71 | +import shutil |
72 | +import stat |
73 | +import subprocess |
74 | +import tempfile |
75 | +import unittest |
76 | + |
77 | +from utah import template |
78 | + |
79 | + |
80 | +class TestAutoRun(unittest.TestCase): |
81 | + |
82 | + """Tests the functionality of the utah-autorun.sh script.""" |
83 | + |
84 | + def setUp(self): |
85 | + """Set up a temporary work directory for the script.""" |
86 | + self.workdir = tempfile.mkdtemp(prefix='utah-autotest-') |
87 | + # ensure we can safely ignore directories |
88 | + os.mkdir('{}/foo'.format(self.workdir)) |
89 | + |
90 | + #piggy back the foo directory for the run script location |
91 | + self.script = os.path.join(self.workdir, 'foo', 'utah-autorun.sh') |
92 | + lf = '/dev/null' |
93 | + template.write('utah-autorun.sh.jinja2', self.script, log_file=lf) |
94 | + os.chmod(self.script, stat.S_IRWXU) |
95 | + |
96 | + self._write_script('01_foo', 'echo foo running') |
97 | + self._write_script('02_bar', 'echo bar running') |
98 | + |
99 | + def tearDown(self): |
100 | + """Clean up the work directory.""" |
101 | + shutil.rmtree(self.workdir) |
102 | + |
103 | + def _write_script(self, name, contents): |
104 | + with open('{}/{}'.format(self.workdir, name), 'w') as f: |
105 | + f.write('#!/bin/sh\n') |
106 | + f.write(contents) |
107 | + os.fchmod(f.fileno(), stat.S_IRWXU) |
108 | + |
109 | + def test_autorun(self): |
110 | + """minimal test to make sure autorun works.""" |
111 | + env = {'WORKDIR': self.workdir, 'RESULTDIR': self.workdir} |
112 | + output = subprocess.check_output([self.script], env=env) |
113 | + self.assertEqual(output, "foo running\nbar running\n") |
114 | + entries = sorted(os.listdir(os.path.join(self.workdir, 'complete'))) |
115 | + self.assertIsNotNone(re.match(r'01_foo\.\d+', entries[0])) |
116 | + self.assertIsNotNone(re.match(r'02_bar\.\d+', entries[1])) |
117 | + self.assertEqual(len(entries), 2) |
118 | + entries = sorted(os.listdir(self.workdir)) |
119 | + self.assertEqual(entries[0], 'complete') |
120 | + self.assertEqual(entries[1], 'foo') |
121 | + self.assertEqual(entries[2], 'inprogress') |
122 | + self.assertEqual(len(entries), 3) |
123 | |
124 | === renamed file 'tests/test_autorun.py' => 'tests/test_autorun.py.moved' |
125 | === added file 'tests/test_config.py' |
126 | --- tests/test_config.py 1970-01-01 00:00:00 +0000 |
127 | +++ tests/test_config.py 2013-06-07 18:58:24 +0000 |
128 | @@ -0,0 +1,105 @@ |
129 | +# Ubuntu Testing Automation Harness |
130 | +# Copyright 2013 Canonical Ltd. |
131 | + |
132 | +# This program is free software: you can redistribute it and/or modify it |
133 | +# under the terms of the GNU General Public License version 3, as published |
134 | +# by the Free Software Foundation. |
135 | + |
136 | +# This program is distributed in the hope that it will be useful, but |
137 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
138 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
139 | +# PURPOSE. See the GNU General Public License for more details. |
140 | + |
141 | +# You should have received a copy of the GNU General Public License along |
142 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
143 | + |
144 | +"""Test configuration module.""" |
145 | + |
146 | +import io |
147 | +import unittest |
148 | + |
149 | +import json |
150 | +import jsonschema |
151 | + |
152 | +from mock import patch |
153 | + |
154 | +import utah.config |
155 | + |
156 | + |
157 | +class TestConfig(unittest.TestCase): |
158 | + |
159 | + """Test configuration functionality.""" |
160 | + |
161 | + def test_defaults_validate(self): |
162 | + """Verify that defaults validate against the schema.""" |
163 | + config = utah.config._Config() |
164 | + jsonschema.validate(dict(config), config.SCHEMA) |
165 | + |
166 | + def test_private_variables_not_in_dict(self): |
167 | + """Verify there are no private variables when converting to a dict.""" |
168 | + config = utah.config._Config() |
169 | + config._private = '<value>' |
170 | + dictionary = dict(config) |
171 | + self.assertNotIn('_private', dictionary) |
172 | + |
173 | + def test_invalid_json_file(self): |
174 | + """Verify error is printed to stderr on invalid json file.""" |
175 | + config = utah.config._Config() |
176 | + |
177 | + config_file = '<config_file>' |
178 | + with patch('utah.config.os.path') as path, \ |
179 | + patch('__builtin__.open') as open_, \ |
180 | + patch('sys.stderr') as stderr: |
181 | + path.is_file.return_value = True |
182 | + path.getsize.return_value = 1 |
183 | + open_.return_value = io.StringIO(u'not a valid json file') |
184 | + |
185 | + config._process_config_file(config_file) |
186 | + |
187 | + stderr.write.assert_called_with( |
188 | + 'ERROR: {} is not a valid JSON file\n'.format(config_file)) |
189 | + |
190 | + def test_invalid_config_file(self): |
191 | + """Verify error is printed to stderr on invalid configuration file.""" |
192 | + config = utah.config._Config() |
193 | + |
194 | + config_file = '<config_file>' |
195 | + unknown_key = 'unknown_key' |
196 | + with patch('utah.config.os.path') as path, \ |
197 | + patch('__builtin__.open') as open_, \ |
198 | + patch('sys.stderr') as stderr: |
199 | + path.isfile.return_value = True |
200 | + path.getsize.return_value = 1 |
201 | + contents = unicode(json.dumps({unknown_key: 'value'})) |
202 | + open_.return_value = io.StringIO(contents) |
203 | + config._process_config_file(config_file) |
204 | + |
205 | + stderr.write.assert_called_with( |
206 | + 'ERROR: {} is not a valid configuration file\n' |
207 | + 'Detailed information: Additional properties are not allowed ' |
208 | + "(u'{}' was unexpected)\n" |
209 | + .format(config_file, unknown_key)) |
210 | + |
211 | + def test_value_overwritten(self): |
212 | + """Verify warning when configuration value is overwritten.""" |
213 | + config = utah.config._Config() |
214 | + |
215 | + config_filenames = ['config_file_1', 'config_file_2'] |
216 | + option_name = 'arch' |
217 | + option_value = 'amd64' |
218 | + with patch('utah.config.os.path') as path, \ |
219 | + patch('__builtin__.open') as open_, \ |
220 | + patch('sys.stderr') as stderr: |
221 | + path.isfile.return_value = True |
222 | + path.getsize.return_value = 1 |
223 | + contents = unicode(json.dumps({option_name: option_value})) |
224 | + open_.side_effect = ( |
225 | + io.StringIO(contents), |
226 | + io.StringIO(contents), |
227 | + ) |
228 | + config._process_config_file(config_filenames[0]) |
229 | + config._process_config_file(config_filenames[1]) |
230 | + |
231 | + stderr.write.assert_called_with( |
232 | + 'WARNING: {} was already set by {}' |
233 | + .format(option_name, config_filenames[0])) |
234 | |
235 | === renamed file 'tests/test_config.py' => 'tests/test_config.py.moved' |
236 | === added file 'tests/test_provision_data.py' |
237 | --- tests/test_provision_data.py 1970-01-01 00:00:00 +0000 |
238 | +++ tests/test_provision_data.py 2013-06-07 18:58:24 +0000 |
239 | @@ -0,0 +1,92 @@ |
240 | +# Ubuntu Testing Automation Harness |
241 | +# Copyright 2013 Canonical Ltd. |
242 | + |
243 | +# This program is free software: you can redistribute it and/or modify it |
244 | +# under the terms of the GNU General Public License version 3, as published |
245 | +# by the Free Software Foundation. |
246 | + |
247 | +# This program is distributed in the hope that it will be useful, but |
248 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
249 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
250 | +# PURPOSE. See the GNU General Public License for more details. |
251 | + |
252 | +# You should have received a copy of the GNU General Public License along |
253 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
254 | + |
255 | +"""Unit tests for the utah.provisioning.data module.""" |
256 | + |
257 | +import os |
258 | +import shutil |
259 | +import subprocess |
260 | +import tempfile |
261 | +import unittest |
262 | + |
263 | +from utah.provisioning.data import ProvisionData |
264 | + |
265 | + |
266 | +class TestProvisionData(unittest.TestCase): |
267 | + |
268 | + """Tests the functions of the ProvisionData class.""" |
269 | + |
270 | + def setUp(self): |
271 | + """Create temp files needed for testing.""" |
272 | + self.files = [] |
273 | + for x in xrange(4): |
274 | + fd, f = tempfile.mkstemp(prefix='utah_provision_data') |
275 | + self.files.append(f) |
276 | + os.write(fd, '{}'.format(x)) |
277 | + os.close(fd) |
278 | + |
279 | + self.initrd = tempfile.mkdtemp(prefix='utah_initrd') |
280 | + |
281 | + def tearDown(self): |
282 | + """Clean up the artifacts create in setUp.""" |
283 | + for f in self.files: |
284 | + os.unlink(f) |
285 | + shutil.rmtree(self.initrd) |
286 | + |
287 | + def test_add_files(self): |
288 | + """Ensure the add_files API works.""" |
289 | + pd = ProvisionData() |
290 | + pd.add_files(self.files[0], '/foo') |
291 | + pd.add_files((self.files[1], self.files[2]), '/bar') |
292 | + |
293 | + pd.update_initrd(self.initrd) |
294 | + |
295 | + dstdir = os.path.join(self.initrd, 'utah-copy-files') |
296 | + for x in xrange(3): |
297 | + bname = os.path.basename(self.files[x]) |
298 | + dstfile = os.path.join(dstdir, bname) |
299 | + self.assertTrue(os.path.isfile(dstfile)) |
300 | + with open(dstfile) as f: |
301 | + content = f.read() |
302 | + self.assertEqual(content, str(x)) |
303 | + |
304 | + script = os.path.join(self.initrd, 'utah-copy-files.sh') |
305 | + self.assertTrue(os.path.isfile(script)) |
306 | + |
307 | + #update the copy script to copy to our tmpdir so we can run locally |
308 | + escaped_dir = self.initrd.replace('/', '\/') |
309 | + exp = 's/\/target/{}/g'.format(escaped_dir) |
310 | + subprocess.check_call(['sed', '-i', '-e', exp, script]) |
311 | + |
312 | + escaped_dir = self.initrd.replace('/', '\/') |
313 | + exp = 's/\/utah-copy-files/{}\/utah-copy-files/g'.format(escaped_dir) |
314 | + subprocess.check_call(['sed', '-i', '-e', exp, script]) |
315 | + |
316 | + subprocess.check_call(['sh', script]) |
317 | + |
318 | + def test_add_cmds(self): |
319 | + """Ensure the add_autostart_cmd API works.""" |
320 | + pd = ProvisionData() |
321 | + for x in xrange(3): |
322 | + pd.add_autostart_cmd('{}_foo'.format(x), str(x)) |
323 | + |
324 | + pd.update_initrd(self.initrd) |
325 | + for x in xrange(3): |
326 | + fname = os.path.join( |
327 | + self.initrd, 'utah-autorun', '{}_foo'.format(x)) |
328 | + self.assertTrue(os.path.isfile(fname)) |
329 | + with open(fname) as f: |
330 | + cmd = f.readlines()[-1] |
331 | + self.assertEqual(cmd, '{}\n'.format(x)) |
332 | |
333 | === renamed file 'tests/test_provision_data.py' => 'tests/test_provision_data.py.moved' |
334 | === modified file 'utah/client/runner.py' |
335 | --- utah/client/runner.py 2013-06-06 22:52:50 +0000 |
336 | +++ utah/client/runner.py 2013-06-07 18:58:24 +0000 |
337 | @@ -432,6 +432,7 @@ |
338 | # fetch failure but allow the test runs to proceed. |
339 | if self.result.status != 'PASS': |
340 | self.result.status = 'PASS' |
341 | +<<<<<<< TREE |
342 | return fetch_success |
343 | |
344 | def process_master_runlist(self, runlist=None, resume=False): |
345 | @@ -448,17 +449,20 @@ |
346 | |
347 | # Download runlist using the URL passed through the commmand line to |
348 | # local filename if needed (it might be a local file already or cached) |
349 | +======= |
350 | + return fetch_success |
351 | + |
352 | + @staticmethod |
353 | + def _fetch_and_parse(runlist): |
354 | +>>>>>>> MERGE-SOURCE |
355 | try: |
356 | local_filename = urllib.urlretrieve(runlist)[0] |
357 | - except IOError: |
358 | - raise exceptions.MissingFile(runlist) |
359 | - except urllib.ContentTooShortError as err: |
360 | + except IOError as err: |
361 | raise exceptions.MissingFile( |
362 | - 'Error when downloading {} (probably interrupted): {}' |
363 | - .format(runlist, err)) |
364 | + 'Error when downloading {}: {}'.format(runlist, err)) |
365 | |
366 | data = parse_yaml_file(local_filename) |
367 | - validator = DefaultValidator(self.MASTER_RUNLIST_SCHEMA) |
368 | + validator = DefaultValidator(Runner.MASTER_RUNLIST_SCHEMA) |
369 | try: |
370 | validator.validate(data) |
371 | except jsonschema.ValidationError as exception: |
372 | @@ -466,15 +470,56 @@ |
373 | 'Master runlist failed to validate: {!r}\n' |
374 | 'Detailed information: {}' |
375 | .format(local_filename, exception)) |
376 | + return data |
377 | + |
378 | + @staticmethod |
379 | + def _clean_suite(name): |
380 | + # convert to absolute name to make troubleshooting a little easier |
381 | + name = os.path.abspath(name) |
382 | + try: |
383 | + shutil.rmtree(name) |
384 | + except OSError as e: |
385 | + raise exceptions.UTAHClientError( |
386 | + 'Error removing the testsuite {}: {}'.format(name, e)) |
387 | + |
388 | + def _add_suite(self, suite): |
389 | + name = suite['name'] |
390 | + |
391 | + if name not in self.fetched_suites: |
392 | + if self._fetch_suite(suite): |
393 | + self.fetched_suites.append(name) |
394 | + suite_runlist = suite.get('runlist', DEFAULT_TSLIST) |
395 | + |
396 | + s = TestSuite(name=name, |
397 | + runlist_file=suite_runlist, |
398 | + includes=suite.get('include_tests', None), |
399 | + excludes=suite.get('exclude_tests', None), |
400 | + result=self.result, |
401 | + path=self.testsuitedir, |
402 | + timeout=self.timeout, |
403 | + battery_measurements=self.battery_measurements, |
404 | + _save_state_callback=self.save_state, |
405 | + _reboot_callback=self.reboot) |
406 | + self.add_suite(s) |
407 | + else: |
408 | + self.fetch_errors += 1 |
409 | + |
410 | + def process_master_runlist(self, runlist=None, resume=False): |
411 | + """Parse a master runlist and build a list of suites from the data. |
412 | + |
413 | + :param runlist: URL pointing to a runlist |
414 | + :type rulist: string |
415 | + :param resume: Continue previous execution |
416 | + :type resume: boolean |
417 | + |
418 | + """ |
419 | + runlist = runlist or self.master_runlist |
420 | + data = self._fetch_and_parse(runlist) |
421 | |
422 | if 'timeout' in data: |
423 | self.timeout = int(data['timeout']) |
424 | |
425 | - if 'name' in data: |
426 | - self.name = data['name'] |
427 | - else: |
428 | - self.name = 'unnamed' |
429 | - |
430 | + self.name = data.get('name', 'unnamed') |
431 | self.battery_measurements = data['battery_measurements'] |
432 | self.repeat_count = data['repeat_count'] |
433 | |
434 | @@ -482,13 +527,7 @@ |
435 | |
436 | orig_dir = os.getcwd() |
437 | |
438 | - if 'testsuites' in data: |
439 | - suites = data['testsuites'] |
440 | - elif isinstance(data, list): |
441 | - suites = data |
442 | - else: |
443 | - raise exceptions.BadMasterRunlist(str(data)) |
444 | - |
445 | + suites = data['testsuites'] |
446 | suites = data['testsuites'] if 'testsuites' in data else data |
447 | for suite in suites: |
448 | # Allow the inclusion of other master.run files |
449 | @@ -500,11 +539,6 @@ |
450 | |
451 | name = suite['name'] |
452 | |
453 | - includes = suite.get('include_tests') |
454 | - excludes = suite.get('exclude_tests') |
455 | - |
456 | - suite_runlist = suite.get('runlist', DEFAULT_TSLIST) |
457 | - |
458 | if name in seen: |
459 | raise exceptions.BadMasterRunlist( |
460 | "{} duplicated in runlist".format(name)) |
461 | @@ -512,38 +546,10 @@ |
462 | # Fetch the testsuite. On resume don't remove the testsuite |
463 | # directory. |
464 | if not resume and os.path.exists(name): |
465 | - # Using absolute name makes no difference |
466 | - # except on failures where it's easier |
467 | - # to find troubleshoot permission problems |
468 | - absolute_name = os.path.abspath(name) |
469 | - try: |
470 | - shutil.rmtree(absolute_name) |
471 | - except OSError as err: |
472 | - raise exceptions.UTAHClientError( |
473 | - 'Error removing the testsuite directory {}: {}' |
474 | - .format(absolute_name, err)) |
475 | + self._clean_suite(name) |
476 | + |
477 | mkdir(name) |
478 | - |
479 | - if name not in self.fetched_suites: |
480 | - if not self._fetch_suite(suite): |
481 | - # If fetch failed move on to the next testsuite. |
482 | - self.fetch_errors += 1 |
483 | - continue |
484 | - |
485 | - self.fetched_suites.append(name) |
486 | - |
487 | - # Create a TestSuite |
488 | - s = TestSuite(name=name, |
489 | - runlist_file=suite_runlist, |
490 | - includes=includes, |
491 | - excludes=excludes, |
492 | - result=self.result, |
493 | - path=self.testsuitedir, |
494 | - timeout=self.timeout, |
495 | - battery_measurements=self.battery_measurements, |
496 | - _save_state_callback=self.save_state, |
497 | - _reboot_callback=self.reboot) |
498 | - self.add_suite(s) |
499 | + self._add_suite(suite) |
500 | |
501 | def get_next_suite(self): |
502 | """Return the next suite to be run. |
503 | |
504 | === added file 'utah/client/tests/test_client.py' |
505 | --- utah/client/tests/test_client.py 1970-01-01 00:00:00 +0000 |
506 | +++ utah/client/tests/test_client.py 2013-06-07 18:58:24 +0000 |
507 | @@ -0,0 +1,71 @@ |
508 | +# Ubuntu Testing Automation Harness |
509 | +# Copyright 2013 Canonical Ltd. |
510 | + |
511 | +# This program is free software: you can redistribute it and/or modify it |
512 | +# under the terms of the GNU General Public License version 3, as published |
513 | +# by the Free Software Foundation. |
514 | + |
515 | +# This program is distributed in the hope that it will be useful, but |
516 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
517 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
518 | +# PURPOSE. See the GNU General Public License for more details. |
519 | + |
520 | +# You should have received a copy of the GNU General Public License along |
521 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
522 | + |
523 | +"""Tests for the client script.""" |
524 | + |
525 | +import os |
526 | +import sys |
527 | +import tempfile |
528 | +import unittest |
529 | + |
530 | +from mock import patch |
531 | + |
532 | +import client |
533 | + |
534 | +from utah.client.common import ReturnCodes |
535 | + |
536 | + |
537 | +class TestClient(unittest.TestCase): |
538 | + |
539 | + """Test the client script.""" |
540 | + |
541 | + @patch('client.sys.stderr') |
542 | + @patch('utah.client.runner.parse_yaml_file') |
543 | + @patch('utah.client.runner.urllib.urlretrieve') |
544 | + @patch('utah.client.runner.os.path.isfile') |
545 | + @patch('client.url_argument') |
546 | + def test_report_not_written_on_invalid_runlist( |
547 | + self, url_argument, isfile, urlretrieve, parse_yaml_file, stderr): |
548 | + """Test report file is not created on runlist validation failure.""" |
549 | + output_file = tempfile.mktemp(prefix='utah-', suffix='.yaml') |
550 | + |
551 | + # Set mock objects return value |
552 | + url_argument.return_value = '<runlist>' |
553 | + isfile.return_value = True |
554 | + filename = '<local_filename>' |
555 | + urlretrieve.return_value = [filename] |
556 | + parse_yaml_file.return_value = 'invalid_runlist' |
557 | + |
558 | + with self.assertRaises(SystemExit): |
559 | + client.main(['-r', '<runlist>', '-o', output_file]) |
560 | + self.assertFalse(os.path.exists(output_file), |
561 | + 'Output file was created: {}'.format(output_file)) |
562 | + args, kwargs = stderr.write.call_args_list[0] |
563 | + self.assertTrue( |
564 | + args[0].startswith('Master runlist failed to validate: ' |
565 | + "'{}'\n".format(filename))) |
566 | + |
567 | + @patch('argparse._sys.stderr') |
568 | + def test_invalid_argument_return_code(self, stderr): |
569 | + """Test return code value on invalid command line arguments.""" |
570 | + prog = 'utah' |
571 | + sys.argv[0] = prog |
572 | + with self.assertRaises(SystemExit) as cm: |
573 | + client.main(['--invalid-option']) |
574 | + |
575 | + self.assertEqual(cm.exception.code, ReturnCodes.CMD_PARSING_ERROR) |
576 | + stderr.write.assert_called_with( |
577 | + '{}: error: unrecognized arguments: --invalid-option\n' |
578 | + .format(prog)) |
579 | |
580 | === renamed file 'utah/client/tests/test_client.py' => 'utah/client/tests/test_client.py.moved' |
581 | === modified file 'utah/client/tests/test_runner.py' |
582 | --- utah/client/tests/test_runner.py 2013-06-03 16:27:13 +0000 |
583 | +++ utah/client/tests/test_runner.py 2013-06-07 18:58:24 +0000 |
584 | @@ -18,6 +18,7 @@ |
585 | |
586 | import os |
587 | import shutil |
588 | +import tempfile |
589 | import unittest |
590 | from mock import patch |
591 | |
592 | @@ -59,21 +60,16 @@ |
593 | state_agent=self.state_agent, |
594 | output=self.output_file) |
595 | self.bad_dir = "/tmp/does_not_exist" |
596 | + _, self.tmpfile = tempfile.mkstemp() |
597 | |
598 | self.assertFalse(os.path.exists(self.bad_dir), |
599 | "{} shouldn't exist".format(self.bad_dir)) |
600 | |
601 | def tearDown(self): |
602 | """Clean up test resources.""" |
603 | - # Remove self.bad_dir if it got created. |
604 | - try: |
605 | + os.unlink(self.tmpfile) |
606 | + if os.path.exists(self.bad_dir): |
607 | shutil.rmtree(self.bad_dir) |
608 | - except OSError as e: |
609 | - # ignore 'No such file or directory errors |
610 | - if e.errno == 2: |
611 | - pass |
612 | - else: |
613 | - raise |
614 | |
615 | def test_fetch_failed(self): |
616 | """Test handling of failed fetch commands.""" |
617 | @@ -238,6 +234,16 @@ |
618 | self.assertListEqual(kwargs['excludes'], |
619 | ['test_1', 'test_2', 'test_3']) |
620 | |
621 | + def test_fetch_runlist(self): |
622 | + """Ensure proper error handling of runlist fetching logic.""" |
623 | + with self.assertRaises(exceptions.MissingFile): |
624 | + Runner._fetch_and_parse('this is a bad url') |
625 | + |
626 | + with self.assertRaises(exceptions.ValidationError): |
627 | + with open(self.tmpfile, 'w') as f: |
628 | + f.write('andy: blah\n') |
629 | + Runner._fetch_and_parse(self.tmpfile) |
630 | + |
631 | |
632 | class TestRunnerMasterRunlistSchema(unittest.TestCase): |
633 | |
634 | |
635 | === modified file 'utah/client/testsuite.py' |
636 | --- utah/client/testsuite.py 2013-06-02 18:08:30 +0000 |
637 | +++ utah/client/testsuite.py 2013-06-07 18:58:24 +0000 |
638 | @@ -37,17 +37,24 @@ |
639 | from utah.client.testcase import TestCase |
640 | |
641 | |
642 | -def parse_runlist_file(runlist_file): |
643 | +def _parse_runlist_file(runlist_file): |
644 | """Parse a tslist.run runlist file and check against schema. |
645 | |
646 | :returns: Parsed data from the runlist |
647 | :rtype: dict |
648 | |
649 | """ |
650 | + if not os.path.exists(runlist_file): |
651 | + raise exceptions.MissingFile('File not found: {}'.format(runlist_file)) |
652 | + |
653 | with open(runlist_file, 'r') as fp: |
654 | data = yaml.load(fp) |
655 | - |
656 | - jsonschema.validate(data, TestSuite.RUNLIST_SCHEMA) |
657 | + try: |
658 | + jsonschema.validate(data, TestSuite.RUNLIST_SCHEMA) |
659 | + except jsonschema.ValidationError as exception: |
660 | + raise exceptions.ValidationError( |
661 | + 'Test suite runlist invalid: {!r}\n' |
662 | + 'Detailed information: {}'.format(runlist_file, exception)) |
663 | |
664 | return data |
665 | |
666 | @@ -147,12 +154,13 @@ |
667 | self.save_state_callback = _save_state_callback |
668 | |
669 | self.result = result |
670 | - |
671 | - control_data = None |
672 | - |
673 | - if _control_data is not None: |
674 | - control_data = _control_data |
675 | - elif self.control_file is not None: |
676 | + self._initialize_control_data(_control_data, control_file) |
677 | + |
678 | + for test in self._get_runlist_data(_runlist_data): |
679 | + self._add_test(test, includes, excludes) |
680 | + |
681 | + def _initialize_control_data(self, control_data, control_file): |
682 | + if not control_data and self.control_file is not None: |
683 | try: |
684 | control_data = parse_control_file(self.control_file, |
685 | self.CONTROL_SCHEMA) |
686 | @@ -173,57 +181,41 @@ |
687 | |
688 | self.__dict__.update(control_data) |
689 | |
690 | - if _runlist_data is not None: |
691 | - runlist_data = _runlist_data |
692 | - elif os.path.exists(self.runlist_file): |
693 | - try: |
694 | - runlist_data = parse_runlist_file(self.runlist_file) |
695 | - except jsonschema.ValidationError as exception: |
696 | - raise exceptions.ValidationError( |
697 | - '{!r} test suite runlist invalid: {!r}\n' |
698 | - 'Detailed information: {}' |
699 | - .format(self.name, self.runlist_file, exception)) |
700 | - else: |
701 | - raise exceptions.MissingFile( |
702 | - 'File not found: {}'.format(self.runlist_file)) |
703 | - |
704 | - for test in runlist_data: |
705 | - name = test['test'] |
706 | - test_path = os.path.join(self.name, name) |
707 | - |
708 | - if includes is not None and name not in includes: |
709 | - continue |
710 | - |
711 | - if excludes is not None and name in excludes: |
712 | - continue |
713 | - |
714 | - command = test.get('command') |
715 | - |
716 | - tc = TestCase(name=name, |
717 | - path=test_path, |
718 | - command=command, |
719 | - timeout=self.timeout, |
720 | - result=self.result, |
721 | - battery_measurements=self.battery_measurements, |
722 | - _save_state_callback=self.save_state_callback, |
723 | - _reboot_callback=self.reboot_callback) |
724 | - |
725 | - runlist_properties = self.RUNLIST_SCHEMA['items']['properties'] |
726 | - override_properties = \ |
727 | - runlist_properties['overrides']['properties'].keys() |
728 | - overrides = {} |
729 | - for property_name in override_properties: |
730 | - if property_name in test: |
731 | - overrides[property_name] = test[property_name] |
732 | - if 'overrides' in test: |
733 | - if self.timeout is not None: |
734 | - test['overrides']['timeout'] = self.timeout |
735 | - overrides.update(test['overrides']) |
736 | - |
737 | - if overrides: |
738 | - tc.process_overrides(overrides) |
739 | - |
740 | - self.tests.append(tc) |
741 | + def _get_runlist_data(self, runlist_data): |
742 | + if runlist_data: |
743 | + return runlist_data |
744 | + return _parse_runlist_file(self.runlist_file) |
745 | + |
746 | + def _add_test(self, test, incs, excs): |
747 | + name = test['test'] |
748 | + |
749 | + if (incs and name not in incs) or (excs and name in excs): |
750 | + return |
751 | + |
752 | + tc = TestCase(name=name, |
753 | + path=os.path.join(self.name, name), |
754 | + command=test.get('command'), |
755 | + timeout=self.timeout, |
756 | + result=self.result, |
757 | + battery_measurements=self.battery_measurements, |
758 | + _save_state_callback=self.save_state_callback, |
759 | + _reboot_callback=self.reboot_callback) |
760 | + |
761 | + runlist_properties = self.RUNLIST_SCHEMA['items']['properties'] |
762 | + override_properties = \ |
763 | + runlist_properties['overrides']['properties'].keys() |
764 | + overrides = {} |
765 | + for property_name in override_properties: |
766 | + if property_name in test: |
767 | + overrides[property_name] = test[property_name] |
768 | + if 'overrides' in test: |
769 | + if self.timeout is not None: |
770 | + test['overrides']['timeout'] = self.timeout |
771 | + overrides.update(test['overrides']) |
772 | + |
773 | + tc.process_overrides(overrides) |
774 | + |
775 | + self.tests.append(tc) |
776 | |
777 | def __str__(self): |
778 | return "{}: {}".format(self.control_file, self.runlist_file) |
779 | |
780 | === added file 'utah/experimental.py' |
781 | --- utah/experimental.py 1970-01-01 00:00:00 +0000 |
782 | +++ utah/experimental.py 2013-06-07 18:58:24 +0000 |
783 | @@ -0,0 +1,48 @@ |
784 | +# Ubuntu Testing Automation Harness |
785 | +# Copyright 2013 Canonical Ltd. |
786 | + |
787 | +# This program is free software: you can redistribute it and/or modify it |
788 | +# under the terms of the GNU General Public License version 3, as published |
789 | +# by the Free Software Foundation. |
790 | + |
791 | +# This program is distributed in the hope that it will be useful, but |
792 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
793 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
794 | +# PURPOSE. See the GNU General Public License for more details. |
795 | + |
796 | +# You should have received a copy of the GNU General Public License along |
797 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
798 | + |
799 | +"""Allows conditional inclusion of experimental features.""" |
800 | + |
801 | +import logging |
802 | +import os |
803 | + |
804 | +_features = [ |
805 | + 'PROVISIONED_AUTORUN', |
806 | +] |
807 | + |
808 | + |
809 | +class _feature(object): |
810 | + |
811 | + """Builds up a list of what experimental features are enabled. |
812 | + |
813 | + Experimental features can be enabled by the existance of an environment |
814 | + variable in the format: UTAH_FEATURE_FOO=<any value> |
815 | + |
816 | + """ |
817 | + |
818 | + def __init__(self): |
819 | + for f in _features: |
820 | + key = 'UTAH_FEATURE_{}'.format(f) |
821 | + setattr(self, f, key in os.environ) |
822 | + |
823 | + def log_features(self): |
824 | + """Writes an info logging message with the state of each feature.""" |
825 | + buf = 'UTAH Features:' |
826 | + for f in _features: |
827 | + buf += '\n {}: {}'.format(f, getattr(self, f)) |
828 | + logging.info(buf) |
829 | + |
830 | + |
831 | +feature = _feature() |
832 | |
833 | === renamed file 'utah/experimental.py' => 'utah/experimental.py.moved' |
834 | === added file 'utah/provisioning/data.py' |
835 | --- utah/provisioning/data.py 1970-01-01 00:00:00 +0000 |
836 | +++ utah/provisioning/data.py 2013-06-07 18:58:24 +0000 |
837 | @@ -0,0 +1,80 @@ |
838 | +# Ubuntu Testing Automation Harness |
839 | +# Copyright 2012 Canonical Ltd. |
840 | + |
841 | +# This program is free software: you can redistribute it and/or modify it |
842 | +# under the terms of the GNU General Public License version 3, as published |
843 | +# by the Free Software Foundation. |
844 | + |
845 | +# This program is distributed in the hope that it will be useful, but |
846 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
847 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
848 | +# PURPOSE. See the GNU General Public License for more details. |
849 | + |
850 | +# You should have received a copy of the GNU General Public License along |
851 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
852 | + |
853 | +"""Data structure to help simplify provisioning logic.""" |
854 | + |
855 | +import os |
856 | +import shutil |
857 | + |
858 | + |
859 | +class ProvisionData(object): |
860 | + |
861 | + """A structure to hold information about image customizations needed. |
862 | + |
863 | + This can be used to inform our provisioning logic about extra files and |
864 | + auto-start commands it should set up for the target |
865 | + |
866 | + """ |
867 | + |
868 | + def __init__(self): |
869 | + self._files = [] |
870 | + self._cmds = [] |
871 | + |
872 | + def add_files(self, files, target_dir): |
873 | + """Add a file to get copied to the target system.""" |
874 | + if isinstance(files, basestring): |
875 | + files = [files] |
876 | + self._files.append({'files': files, 'target_dir': target_dir}) |
877 | + |
878 | + def add_autostart_cmd(self, name, cmd): |
879 | + """Add a command to be run at startup.""" |
880 | + self._cmds.append({'name': name, 'cmd': cmd}) |
881 | + |
882 | + def _update_initrd_files(self, path): |
883 | + copy_dir = os.path.join(path, 'utah-copy-files') |
884 | + os.mkdir(copy_dir) |
885 | + |
886 | + with open(os.path.join(path, 'utah-copy-files.sh'), 'w') as f: |
887 | + f.write('#!/bin/sh\n') |
888 | + for fileobj in self._files: |
889 | + tgt_dst = '/target{}'.format(fileobj['target_dir']) |
890 | + f.write('[ -d {d} ] || mkdir -p {d}\n'.format(d=tgt_dst)) |
891 | + for path in fileobj['files']: |
892 | + fname = os.path.basename(path) |
893 | + hst_cpy = os.path.join(copy_dir, fname) |
894 | + tgt_cpy = os.path.join('/utah-copy-files', fname) |
895 | + shutil.copyfile(path, hst_cpy) |
896 | + f.write('cp {} {}/\n'.format(tgt_cpy, tgt_dst)) |
897 | + |
898 | + def _update_initrd_cmds(self, path): |
899 | + path = os.path.join(path, 'utah-autorun') |
900 | + os.mkdir(path) |
901 | + |
902 | + for cmd in self._cmds: |
903 | + with open(os.path.join(path, cmd['name']), 'w') as f: |
904 | + f.write('#!/bin/sh\n') |
905 | + f.write('{}\n'.format(cmd['cmd'])) |
906 | + |
907 | + def update_initrd(self, path): |
908 | + """Build the initrd file to include needed files commands. |
909 | + |
910 | + This copies the files to the initrd, and builds a script that can be |
911 | + run to put them in the proper directory on the installed system. It |
912 | + also adds a set of auto run files that can be copied over to the |
913 | + installed system during the late command. |
914 | + |
915 | + """ |
916 | + self._update_initrd_files(path) |
917 | + self._update_initrd_cmds(path) |
918 | |
919 | === renamed file 'utah/provisioning/data.py' => 'utah/provisioning/data.py.moved' |
Please could you rebase to fix the merge conflict? The diff here doesn't seem to be properly displayed. For example, master_ runlist` docstring includes parameters, but looking at the branch I don't see that.
it looks like `process_