Merge lp:~javier.collado/utah/bug1082087 into lp:utah

Proposed by Javier Collado
Status: Merged
Approved by: Javier Collado
Approved revision: 871
Merged at revision: 864
Proposed branch: lp:~javier.collado/utah/bug1082087
Merge into: lp:utah
Diff against target: 236 lines (+151/-3)
6 files modified
.bzrignore (+2/-0)
debian/changelog (+1/-0)
docs/source/conf.py (+6/-1)
tests/test_run.py (+70/-0)
utah/run.py (+70/-2)
utah/url.py (+2/-0)
To merge this branch: bzr merge lp:~javier.collado/utah/bug1082087
Reviewer Review Type Date Requested Status
Andy Doan (community) Approve
Javier Collado (community) Needs Resubmitting
Review via email: mp+158927@code.launchpad.net

Description of the change

This branch adds a new function `master_runlist_argument` that includes
`url_argument` together with validation against the master runlist schema. This
way, the server can fail early before provisioning when the runlist doesn't
pass the validation step instead of going through all the provisioning and wait
for the client to fail.

To post a comment you must log in.
Revision history for this message
Javier Collado (javier.collado) wrote :

What the bug asks for is a separate validation command that retrieves not only
the master runlists, but also the test suites and the test cases to validate
everything in one step. Hence, this merge doesn't really fix the bug, but is an
improvement over getting an error in the client after the provisioning that
could have been easily detected in the server at the very beginning.

Revision history for this message
Andy Doan (doanac) wrote :

+1 I think its a nice start. Should we start adding unit testing for this new function in a file like tests/test_run[_arg].py?

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

Yes, I think I should add some test case. Let me work on it and resubmit when I'm done.

lp:~javier.collado/utah/bug1082087 updated
865. By Javier Collado

Updated documentation build to mock modules only when they aren't available

This makes easier to run doctests without causing any problems to the build in
readthedocs.org

866. By Javier Collado

Ignore documentation build files

867. By Javier Collado

Updated url_argument example to include a launchpad url

868. By Javier Collado

Added a usage example for the master_runlist_argument function

The example is also a doctests

869. By Javier Collado

Added master_runlist_argument unittests

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

I've added a doctest with an example of how to use `master_runlist_argument`
and a file with unit tests to make sure that either the filename is returned on
success or ArgumentTypeError is reased on a failure.

review: Needs Resubmitting
lp:~javier.collado/utah/bug1082087 updated
870. By Javier Collado

Fixed ignore patterns

Star character was expanded by the shell by accident.

Revision history for this message
Max Brustkern (nuclearbob) wrote :

When I test things I get:
======================================================================
ERROR: Filename is returned on success.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/max/bzr/merges/bug1082087/tests/test_run.py", line 66, in test_filename_returned
    filename = master_runlist_argument('url')
  File "/home/max/bzr/merges/bug1082087/utah/run.py", line 111, in master_runlist_argument
    filename = url_argument(url)
  File "/usr/lib/python2.7/dist-packages/mock.py", line 341, in __call__
    ret_val = self.side_effect(*args, **kwargs)
TypeError: 'list' object is not callable

----------------------------------------------------------------------
Ran 57 tests in 19.359s

FAILED (errors=1)

I'm looking into whether that's something wrong with my setup.

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

The mock.py I'm looking at doesn't seem to have the same line as in your
backtrace. From what I see that file was installed in my quantal system from
main and the version is 0.8.0-2. Do you have the same version installed?

Revision history for this message
Andy Doan (doanac) wrote :

Not sure, but for the snippets like:

 with self.assertRaises(ArgumentTypeError):

could we use "assertRaisesRegexp" and be sure we are catching the right thing?

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

Yes, I can do that. Indeed, even if the exception is always of the same type,
the message is different depending on the error. Let me add that and resubmit.

lp:~javier.collado/utah/bug1082087 updated
871. By Javier Collado

Added patterns to check error messages in exceptions

The idea is to make sure that exceptions of the same type were raised because
of different reasons as suggested by Andy.

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

Replaced assertRaises with assertRaisesRegexp to make sure about why the
ArgumentTypeError exceptions were raised.

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

Thanks for the suggestion, by the way.

Revision history for this message
Andy Doan (doanac) wrote :

looks nice

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2013-04-16 15:26:28 +0000
4@@ -0,0 +1,2 @@
5+docs/source/man/*.txt
6+docs/build/*
7
8=== modified file 'debian/changelog'
9--- debian/changelog 2013-04-16 13:21:57 +0000
10+++ debian/changelog 2013-04-16 15:26:28 +0000
11@@ -17,6 +17,7 @@
12 * Try to close SFTP client only if it's open (LP: #1163197)
13 * Use sys.exit when SIGTERM is received (LP: #1160247)
14 * Write syslog pattern matched and timeouts timestamps (LP: #1162862)
15+ * Validate master runlist before provisioning (LP: #1082087)
16
17 [ Max Brustkern ]
18 * Cleaned up docstrings to be PEP257 compliant
19
20=== modified file 'docs/source/conf.py'
21--- docs/source/conf.py 2013-04-04 15:39:49 +0000
22+++ docs/source/conf.py 2013-04-16 15:26:28 +0000
23@@ -79,10 +79,15 @@
24
25
26 # Mock all third party modules not available in the readthedocs.org environment
27+# Note that in a development environment where the modules are available, they
28+# are not mocked to make it possible to run the doctests correctly.
29 for mod_name in ('psutil', 'yaml', 'paramiko', 'jsonschema', 'libvirt',
30 'bzrlib', 'bzrlib.builtins', 'bzrlib.plugin', 'bzrlib.errors',
31 'apt', 'apt.cache', 'netifaces'):
32- sys.modules[mod_name] = ModuleMock(mod_name)
33+ try:
34+ __import__(mod_name)
35+ except ImportError:
36+ sys.modules[mod_name] = ModuleMock(mod_name)
37
38
39 def get_parser_strings(script_name, parser):
40
41=== added file 'tests/test_run.py'
42--- tests/test_run.py 1970-01-01 00:00:00 +0000
43+++ tests/test_run.py 2013-04-16 15:26:28 +0000
44@@ -0,0 +1,70 @@
45+# Ubuntu Testing Automation Harness
46+# Copyright 2013 Canonical Ltd.
47+
48+# This program is free software: you can redistribute it and/or modify it
49+# under the terms of the GNU General Public License version 3, as published
50+# by the Free Software Foundation.
51+
52+# This program is distributed in the hope that it will be useful, but
53+# WITHOUT ANY WARRANTY; without even the implied warranties of
54+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
55+# PURPOSE. See the GNU General Public License for more details.
56+
57+# You should have received a copy of the GNU General Public License along
58+# with this program. If not, see <http://www.gnu.org/licenses/>.
59+
60+"""Test command line argument functions."""
61+
62+import unittest
63+
64+from argparse import ArgumentTypeError
65+from jsonschema import ValidationError
66+from mock import patch
67+
68+from utah.client.exceptions import (
69+ YAMLEmptyFile,
70+ YAMLParsingError,
71+)
72+from utah.run import master_runlist_argument
73+
74+
75+class TestMasterRunlistArgument(unittest.TestCase):
76+ """Test master_runlist_argument function."""
77+
78+ @patch('utah.run.parse_yaml_file')
79+ @patch('utah.run.url_argument')
80+ def test_empty_file(self, url_argument, parse_yaml_file):
81+ """ArgumentTypeError is raised on empty file."""
82+ parse_yaml_file.side_effect = YAMLEmptyFile()
83+ pattern = r'Master runlist seems to be empty:'
84+ with self.assertRaisesRegexp(ArgumentTypeError, pattern):
85+ master_runlist_argument('url')
86+
87+ @patch('utah.run.parse_yaml_file')
88+ @patch('utah.run.url_argument')
89+ def test_parsing_error(self, url_argument, parse_yaml_file):
90+ """ArgumentTypeError is raised on parsing error."""
91+ parse_yaml_file.side_effect = YAMLParsingError()
92+ pattern = r'Master runlist failed to parse as a yaml file:'
93+ with self.assertRaisesRegexp(ArgumentTypeError, pattern):
94+ master_runlist_argument('url')
95+
96+ @patch('utah.run.jsonschema.validate')
97+ @patch('utah.run.parse_yaml_file')
98+ @patch('utah.run.url_argument')
99+ def test_validation_error(self, url_argument, parse_yaml_file, validate):
100+ """ArgumentTypeError is raised on validation error."""
101+ validate.side_effect = ValidationError('error description')
102+ pattern = 'Master runlist failed to validate:'
103+ with self.assertRaisesRegexp(ArgumentTypeError, pattern):
104+ master_runlist_argument('url')
105+
106+ @patch('utah.run.jsonschema.validate')
107+ @patch('utah.run.parse_yaml_file')
108+ @patch('utah.run.url_argument')
109+ def test_filename_returned(self, url_argument, parse_yaml_file, validate):
110+ """Filename is returned on success."""
111+ expected_filename = 'filename'
112+ url_argument.side_effect = [expected_filename]
113+ filename = master_runlist_argument('url')
114+ self.assertEqual(filename, expected_filename)
115
116=== modified file 'utah/run.py'
117--- utah/run.py 2013-04-11 19:09:06 +0000
118+++ utah/run.py 2013-04-16 15:26:28 +0000
119@@ -16,6 +16,7 @@
120 """Provide routines used to process command line arguments and run tests."""
121
122
123+from argparse import ArgumentTypeError
124 import logging
125 import os
126 import shutil
127@@ -26,13 +27,23 @@
128 import traceback
129 import urllib
130
131+import jsonschema
132+
133 from utah import config
134+from utah.client.common import (
135+ parse_yaml_file,
136+ ReturnCodes as ClientReturnCodes,
137+)
138+from utah.client.runner import Runner
139+from utah.client.exceptions import (
140+ YAMLEmptyFile,
141+ YAMLParsingError,
142+)
143 from utah.exceptions import UTAHException
144 from utah.retry import retry
145 from utah.timeout import timeout
146 from utah.url import url_argument
147 from utah.provisioning.ssh import ProvisionedMachine
148-from utah.client.common import ReturnCodes as ClientReturnCodes
149
150
151 # Return codes for the server
152@@ -64,6 +75,63 @@
153 return returncode + offset
154
155
156+def master_runlist_argument(url):
157+ """Get master runlist and validate it against its schema.
158+
159+ This function calls :func:`utah.url.url_argument` to download the runlist
160+ file if needed and then uses the schema defined in
161+ :class:`utah.client.runner.Runner` to validate it.
162+
163+ This functions is expected to be passed as the `type` argument of an
164+ `argparse.ArgumentParser` object to make sure the master runlist is
165+ reachable and valid before provisioning the system under test.
166+
167+ :param url: URL as passed to parser object.
168+ :type url: `basestring`
169+ :returns: URL or path to the local file
170+ :rtype: `basestring`
171+
172+ .. seealso::
173+ :func:`utah.url.url_argument`,
174+ :class:`utah.client.runner.Runner`.
175+
176+ :Example:
177+
178+ >>> from utah.run import master_runlist_argument
179+ >>> import argparse
180+ >>> parser = argparse.ArgumentParser()
181+ >>> parser.add_argument('url',
182+ ... type=master_runlist_argument) # doctest: +ELLIPSIS
183+ _StoreAction(... dest='url', ...)
184+ >>> runlist = 'lp:utah/utah/client/examples/pass.run'
185+ >>> parser.parse_args([runlist]) # doctest: +ELLIPSIS
186+ Namespace(url='/tmp/utah_...')
187+
188+ """
189+ filename = url_argument(url)
190+ try:
191+ data = parse_yaml_file(filename)
192+ except YAMLEmptyFile:
193+ raise ArgumentTypeError(
194+ 'Master runlist seems to be empty: {!r}'
195+ .format(filename))
196+ except YAMLParsingError as exception:
197+ raise ArgumentTypeError(
198+ 'Master runlist failed to parse as a yaml file: {!r}\n'
199+ 'Detailed information: {}'
200+ .format(filename, exception))
201+
202+ try:
203+ jsonschema.validate(data, Runner.MASTER_RUNLIST_SCHEMA)
204+ except jsonschema.ValidationError as exception:
205+ raise ArgumentTypeError(
206+ 'Master runlist failed to validate: {!r}\n'
207+ 'Detailed information: {}'
208+ .format(filename, exception))
209+
210+ return filename
211+
212+
213 def common_arguments(parser):
214 """Centralize command line arguments for all run_ scripts.
215
216@@ -72,7 +140,7 @@
217
218 """
219 parser.add_argument('runlist', metavar='runlist',
220- type=url_argument,
221+ type=master_runlist_argument,
222 help='URLs of runlist files to run')
223 parser.add_argument('-s', '--series', metavar='SERIES',
224 choices=config.serieschoices,
225
226=== modified file 'utah/url.py'
227--- utah/url.py 2013-04-03 21:17:26 +0000
228+++ utah/url.py 2013-04-16 15:26:28 +0000
229@@ -196,6 +196,8 @@
230 _StoreAction(... dest='url', ...)
231 >>> parser.parse_args(['http://www.ubuntu.com'])
232 Namespace(url='http://www.ubuntu.com')
233+ >>> parser.parse_args(['lp:utah/setup.py']) # doctest: +ELLIPSIS
234+ Namespace(url='/tmp/utah_...')
235
236 .. seealso:: :class:`URLChecker`
237

Subscribers

People subscribed via source and target branches