Merge lp:~doanac/utah/process_runlist_cleanup into lp:utah

Proposed by Andy Doan
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
Reviewer Review Type Date Requested Status
Andy Doan Pending
Review via email: mp+167347@code.launchpad.net

This proposal supersedes a proposal from 2013-06-02.

This proposal has been superseded by a proposal from 2013-06-07.

Description of the change

This finishes the cleanup of the overly complex process_master_runlist function

To post a comment you must log in.
927. By Andy Doan

fix test-case regression from revno 915.4.8

provisioning.py was fixed, but I failed to fix the test case

928. By Andy Doan

fix some broken test cases

These all broke due to recent API changes

929. By Max Brustkern

Merged new ssh fixes

Revision history for this message
Javier Collado (javier.collado) wrote :

Please could you rebase to fix the merge conflict? The diff here doesn't seem to be properly displayed. For example,
it looks like `process_master_runlist` docstring includes parameters, but looking at the branch I don't see that.

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 ContentTooShortError 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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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'

Subscribers

People subscribed via source and target branches

to all changes: