Merge lp:~nuclearbob/utah/docstring-cleanup into lp:utah

Proposed by Max Brustkern
Status: Merged
Merged at revision: 855
Proposed branch: lp:~nuclearbob/utah/docstring-cleanup
Merge into: lp:utah
Diff against target: 7966 lines (+1933/-2048)
77 files modified
client.py (+7/-8)
debian/changelog (+18/-1)
docs/source/conf.py (+22/-14)
examples/run_install_test.py (+4/-1)
examples/run_test_bamboo_feeder.py (+4/-1)
examples/run_test_cobbler.py (+4/-1)
examples/run_test_vm.py (+4/-1)
examples/run_utah_tests.py (+4/-1)
tests/test_rsyslog.py (+16/-17)
utah-done.py (+1/-0)
utah/__init__.py (+1/-3)
utah/cleanup.py (+5/-4)
utah/client/__init__.py (+1/-3)
utah/client/battery.py (+21/-1)
utah/client/common.py (+59/-298)
utah/client/examples/examples/test_one/test_one.py (+3/-0)
utah/client/examples/examples/test_two/test_two.py (+3/-0)
utah/client/examples/utah_tests/test_one/test_one.py (+3/-0)
utah/client/examples/utah_tests/test_two/test_two.py (+3/-0)
utah/client/examples/utah_tests_sample/sample_one/sample.py (+3/-0)
utah/client/exceptions.py (+37/-26)
utah/client/phoenix.py (+2/-1)
utah/client/result.py (+73/-78)
utah/client/runner.py (+47/-54)
utah/client/state_agent.py (+23/-21)
utah/client/testcase.py (+43/-48)
utah/client/tests/__init__.py (+2/-0)
utah/client/tests/common.py (+19/-15)
utah/client/tests/manual_privileges.py (+0/-188)
utah/client/tests/test_common.py (+36/-21)
utah/client/tests/test_jsonschema.py (+10/-16)
utah/client/tests/test_misc.py (+8/-2)
utah/client/tests/test_phoenix.py (+18/-29)
utah/client/tests/test_result.py (+21/-22)
utah/client/tests/test_runner.py (+17/-18)
utah/client/tests/test_state_agent.py (+17/-21)
utah/client/tests/test_testcase.py (+20/-20)
utah/client/tests/test_testsuite.py (+19/-11)
utah/client/tests/test_vcs_bzr.py (+15/-18)
utah/client/tests/test_vcs_dev.py (+15/-18)
utah/client/tests/test_vcs_git.py (+15/-18)
utah/client/tests/test_yaml.py (+9/-0)
utah/client/testsuite.py (+39/-37)
utah/client/vcs.py (+144/-0)
utah/commandstr.py (+5/-7)
utah/config.py (+12/-10)
utah/exceptions.py (+8/-7)
utah/group.py (+7/-11)
utah/iso.py (+108/-67)
utah/isotest/iso_static_validation.py (+18/-22)
utah/orderedcollections.py (+14/-3)
utah/parser.py (+2/-0)
utah/preseed.py (+6/-0)
utah/process.py (+2/-0)
utah/provisioning/__init__.py (+1/-3)
utah/provisioning/baremetal/__init__.py (+1/-3)
utah/provisioning/baremetal/bamboofeeder.py (+64/-76)
utah/provisioning/baremetal/cobbler.py (+91/-72)
utah/provisioning/baremetal/exceptions.py (+5/-6)
utah/provisioning/baremetal/inventory.py (+38/-19)
utah/provisioning/baremetal/power.py (+20/-22)
utah/provisioning/exceptions.py (+7/-6)
utah/provisioning/inventory/__init__.py (+1/-3)
utah/provisioning/inventory/exceptions.py (+5/-6)
utah/provisioning/inventory/inventory.py (+33/-34)
utah/provisioning/inventory/sqlite.py (+10/-7)
utah/provisioning/provisioning.py (+347/-258)
utah/provisioning/rsyslog.py (+2/-2)
utah/provisioning/ssh.py (+57/-35)
utah/provisioning/vm/__init__.py (+1/-3)
utah/provisioning/vm/exceptions.py (+5/-6)
utah/provisioning/vm/vm.py (+147/-145)
utah/publish.py (+0/-146)
utah/retry.py (+3/-3)
utah/run.py (+55/-15)
utah/timeout.py (+17/-15)
utah/url.py (+6/-0)
To merge this branch: bzr merge lp:~nuclearbob/utah/docstring-cleanup
Reviewer Review Type Date Requested Status
Andy Doan (community) Approve
Max Brustkern (community) Needs Resubmitting
Review via email: mp+157162@code.launchpad.net

Description of the change

This branch cleans up docstrings to be PEP257 compliant, updates string formatting, and addresses a number of minor issues raised during the code review while attempting to minimize potential functionality changes.

To post a comment you must log in.
Revision history for this message
Max Brustkern (nuclearbob) wrote :

I'm still testing provisioning and some client self tests are failing, but I'd still like to get some other eyes on this since it's a big one.

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

All the provisioning bugs are fixed now, so if anyone wants to look things over or help with the client self-test issues, that would be rad.

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

Looks good. How bout we add a unit test to cover the get_children_process change. here's something i hacked together:

 http://paste.ubuntu.com/5680415/

961. By Max Brustkern

Accidentally removed a function when converting to private, reinstating

962. By Max Brustkern

Converting pid to string before passing it to subprocess

963. By Max Brustkern

I misunderstood the difference between revision and get_revision, and get_revision was only used by tests, so I'm using the correct revision as revision now

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

I've fixed all but one of the client test issues. I'm still not raising MissingData, because we decided in the code review to take out the block of code that raises that. I think some other exception should be raised, however, and that doesn't appear to be happening. I think the lack of another exception is not a problem with this merge, but we should either fix that problem, or reinstate the MissingData block before moving forward. I'm in favor of fixing the problem, so I'm going to try for that.

964. By Max Brustkern

utah/client/tests/test_testcase.py

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

I've removed that test. The DefaultValidator isn't working, and I think we should have some different tests for it. I've opened a bug:
https://bugs.launchpad.net/utah/+bug/1165175
and I'll work on that separately. I think we should have probably at least three tests for the DefaultValidator:
test that it validates valid data
test that it fails to validate bad data
test that it populates a default if needed
and maybe
test that not supplying a default doesn't cause validation to fail?

Additionally, in test_testcase, I think we should test for:
missing field
extra field
bad reboot
bad timeout
bad type

I'll add that info to the bug as well.

965. By Max Brustkern

Removed unneeded exception and backed out my testing changes to DefaultValidator

966. By Max Brustkern

Fixed static analysis errors, added test suggested by Andy, removed correct exception

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

> Looks good. How bout we add a unit test to cover the get_children_process
> change. here's something i hacked together:
>
> http://paste.ubuntu.com/5680415/

Added that. Good idea.

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

common.py:validate_properties has some extra "print" statements that seem to be left over from debugging.

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

You removed the MissingData check:

        # Do not proceed if we are missing data
        if (self.description is None
                or self.dependencies is None
                or self.action is None
                or self.expected_results is None):
            raise MissingData

However, I discovered a bug the other data in this same place. Our DefaultValidator is broken and doesn't raise errors when this data is missing. So this can't be removed until we fix that code. I glanced at fixing it, but haven't gotten around to making it work. NOTE: we did our own DefaultValidator so that we could set default attributes on our object.

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

rsyslogd.py
            # TODO: Find out if this can/should be None
            self._port = 0

the TODO line that was added isn't needed. This line is required and safe.

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

should _UTAHISOException really be listed as private? We don't seem to catch this in our code, so I *think* its a user visible exception?

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

I'll fix the print statements.

I removed the MissingData check because we said in the code review that we wanted to remove that since we're using the validator now. The validator code that's in dev right now (and I think production) is already broken, and I don't think removing that check will make things worse. We can leave it in until that's fixed if you want, but either way, we should fix it soon, since we're currently behaving in confusing and bad ways when you try to run a bad tc_control file.

That TODO was for me, and I forget to get to it before I finished the merge. Whoops. I did want to ask you about that, when we're using a file, do we want port to be 0 or would it be better as None? Does the code to send the rsyslog arguments to the kernel command line still check if that value is None? I don't think we need those parameters if we're using a file, so I wanted to check if we were still setting them.

You make a good point about the exception. I think I went a little overboard making things private. I'll roll that back.

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

On 04/08/2013 11:03 AM, Max Brustkern wrote:
> I removed the MissingData check because we said in the code review
> that we wanted to remove that since we're using the validator now.
> The validator code that's in dev right now (and I think production)
> is already broken, and I don't think removing that check will make
> things worse.

If you remove this, then we need to assign those attributes default
values in the schema so that they get set. Otherwise, I think a "bad"
control file could cause us to try and reference variables that haven't
been initialized.

> We can leave it in until that's fixed if you want, but
> either way, we should fix it soon, since we're currently behaving in
> confusing and bad ways when you try to run a bad tc_control file.

I would like to fix it soon. Just not sure we want to fit it in this.

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

On 04/08/2013 11:03 AM, Max Brustkern wrote:
> That TODO was for me, and I forget to get to it before I finished the
> merge. Whoops. I did want to ask you about that, when we're using a
> file, do we want port to be 0 or would it be better as None? Does
> the code to send the rsyslog arguments to the kernel command line
> still check if that value is None? I don't think we need those
> parameters if we're using a file, so I wanted to check if we were
> still setting them.

I think it can be None or 0. If you look at provisioning.py we have a check:

  if self.rsyslog.port:
    ... (setup rsylog'ing)
  else:
    ... (set up serial console logging)

Maybe a smarter thing would be to make this more explicit like:

  if self.rsyslog.local_file()

not sure.

967. By Max Brustkern

Implemented some fixes suggested by Andy

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

> On 04/08/2013 11:03 AM, Max Brustkern wrote:
> > That TODO was for me, and I forget to get to it before I finished the
> > merge. Whoops. I did want to ask you about that, when we're using a
> > file, do we want port to be 0 or would it be better as None? Does
> > the code to send the rsyslog arguments to the kernel command line
> > still check if that value is None? I don't think we need those
> > parameters if we're using a file, so I wanted to check if we were
> > still setting them.
>
> I think it can be None or 0. If you look at provisioning.py we have a check:
>
> if self.rsyslog.port:
> ... (setup rsylog'ing)
> else:
> ... (set up serial console logging)
>
> Maybe a smarter thing would be to make this more explicit like:
>
> if self.rsyslog.local_file()
>
> not sure.

I think I originally wrote things as 'if config.rsyslogport is not None' and I forgot that you had improved on that by testing the value directly, which will return False on a 0. Good stuff. I implemented the three things you said other than the MissingData, which I'll comment on separately.

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

> On 04/08/2013 11:03 AM, Max Brustkern wrote:
> > I removed the MissingData check because we said in the code review
> > that we wanted to remove that since we're using the validator now.
> > The validator code that's in dev right now (and I think production)
> > is already broken, and I don't think removing that check will make
> > things worse.
>
> If you remove this, then we need to assign those attributes default
> values in the schema so that they get set. Otherwise, I think a "bad"
> control file could cause us to try and reference variables that haven't
> been initialized.
>
> > We can leave it in until that's fixed if you want, but
> > either way, we should fix it soon, since we're currently behaving in
> > confusing and bad ways when you try to run a bad tc_control file.
>
> I would like to fix it soon. Just not sure we want to fit it in this.

My feeling is, this is already broken. I don't see a MissingData exception get raised when I feed in a file with bad data, even without that change in place, so I don't think that code is doing anything useful. Maybe that means we should leave it in until the branch that fixes it (which I'm hoping to propose today or tomorrow.) As I'm typing this, that actually makes more sense. If nobody disagrees, I can put that block and the test back in. That will mean the test is failing, but the correction for that is something we're going to want as a bugfix anyway, so landing it separately makes sense.

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

On 04/08/2013 12:33 PM, Max Brustkern wrote:
> My feeling is, this is already broken. I don't see a MissingData
> exception get raised when I feed in a file with bad data, even
> without that change in place, so I don't think that code is doing
> anything useful.

hmm - I've seen it. it was what prompted me to start on:

  https://code.launchpad.net/~doanac/utah/schema-bug/+merge/156215

> Maybe that means we should leave it in until the
> branch that fixes it (which I'm hoping to propose today or tomorrow.)
> As I'm typing this, that actually makes more sense. If nobody
> disagrees, I can put that block and the test back in. That will mean
> the test is failing, but the correction for that is something we're
> going to want as a bugfix anyway, so landing it separately makes
> sense.

okay

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

+1

968. By Max Brustkern

Put MissingData bits back in. We'll resolve that in a separate branch.

969. By Max Brustkern

Static analysis fixed

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

Okay, all of the issues you raised should be fixed now. We can handle the MissingData thing with a new validator in a separate branch.

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

+1 for me.

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

Thanks for all the effort on this branch.

The changes look good and I've been able to successfully execute the pass
runlist. However, there are still a few issues reported by the pep257 tool:
------------
provisioning.py:76:8: PEP257 Return value type should be mentioned.
runner.py:415:8: PEP257 Return value type should be mentioned.
setup.py:1:0: PEP257 Modules should have docstrings.
------------

I don't think they are really important, but if you could fix them before
merging to the development branch, that would be great.

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

I didn't see this before I merged, but the first two are false positives. Those functions both define functions internally that have return statements, but those functions themselves do not have return statements. I think we should separate the internal functions to get rid of those warnings.

As to setup.py, I was mostly ignoring that, along with distribute_setup.py, but I forgot we actually do maintain setup.py. I'll address that issue before release, and we can take care of the other ones later since they involve restructuing.

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

That's interesting, I didn't know why the error was triggered just for some
methods.

Looking at them I see that:
- one of them could be a function at the module level because is defined there
  just for the convenience of having it close to where it's used, but it's not
  really part of the method.
- the other one uses a variable from the outer scope. Hence the signature must
  be change if it's moved as an instance method in the class itself.

I don't like much the second bullet (changing the signature), so I've made a
couple of tests to verify that adding 'return' to the documentation is enough
to get rid of the error message. Consequently, if we stick to use sphinx
markup, then we could just use `:returns: None` after all the `:param:` and
`:type:` tags and the error message would be gone. What do you think about this
option?

By the way, some other detail is that the private methods are not added by
default to the documentation, which I think is fine, but that means that the
documentation for `__init__` sometimes has to be moved to the class docstring
to make it available in the documentation. I've done that for a couple of
classes in the code and I think it applies to the `Machine` class as well.

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

I don't like that second bullet either. `:returns: None` sounds like a good solution.

That makes me wonder what, if any, other private methods we do want documented. We can make another merge proposal to move __init__ documentation up to the class level for things that need it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'client.py'
2--- client.py 2013-04-02 13:56:33 +0000
3+++ client.py 2013-04-08 19:17:22 +0000
4@@ -15,6 +15,9 @@
5 # You should have received a copy of the GNU General Public License along
6 # with this program. If not, see <http://www.gnu.org/licenses/>.
7
8+"UTAH client used to run tests."""
9+
10+
11 import logging
12 import platform
13 import sys
14@@ -33,9 +36,7 @@
15
16
17 def get_parser():
18- """
19- Process the command line arguments.
20- """
21+ """Process the command line arguments."""
22 # requires Python2.7+
23 import argparse
24
25@@ -48,8 +49,8 @@
26 parser.add_argument('--resume', action='store_true',
27 help='Continue a previous run. Used after a reboot')
28 parser.add_argument('-s', '--state-file',
29- help=('File to use for storing state (default "%s")'
30- % DEFAULT_STATE_FILE))
31+ help=('File to use for storing state (default "{}")'
32+ .format(DEFAULT_STATE_FILE)))
33 parser.add_argument('-f', '--format',
34 choices=['text', 'yaml', 'json'],
35 default='yaml',
36@@ -87,9 +88,7 @@
37
38
39 def main():
40- """
41- The main driver for the 'utah' application.
42- """
43+ """Interpret command line arguments and run tests."""
44 # TODO: write <2.7 optparse version and set based on version of python
45 # being used.
46 parser = get_parser()
47
48=== modified file 'debian/changelog'
49--- debian/changelog 2013-04-04 14:58:12 +0000
50+++ debian/changelog 2013-04-08 19:17:22 +0000
51@@ -1,5 +1,6 @@
52 utah (0.10ubuntu1) UNRELEASED; urgency=low
53
54+ [ Javier Collado ]
55 * Return error code on unhandled error (LP: #1160857)
56 * Remove temporary files downloaded based on URL (LP: #1101186)
57 * Stop server on installation failure (LP: #1161855)
58@@ -9,7 +10,23 @@
59 * Use sys.exit when SIGTERM is received (LP: #1160247)
60 * Write syslog pattern matched and timeouts timestamps (LP: #1162862)
61
62- -- Javier Collado <javier.collado@canonical.com> Wed, 27 Mar 2013 13:00:47 +0100
63+ [ Max Brustkern ]
64+ * Cleaned up docstrings to be PEP257 compliant
65+ * Improved string formatting
66+ * Harmonized Machine provision method signatures and returns
67+ * Separated VCS handling into its own file and simplified revision
68+ handling
69+ * Privatized public functions
70+ * Removed publishing code
71+ * Cleaned up import ordering
72+ * Refactored kernel command line processing
73+ * Added local copy of specified kernel and initrd
74+ * Removed useless comments
75+ * Improved exception handling
76+ * Improved logging
77+ * Cleaned up issues raised during code review
78+
79+ -- Max Brustkern <max@canonical.com> Thu, 04 Apr 2013 12:29:54 -0400
80
81 utah (0.9.2ubuntu1) quantal; urgency=low
82
83
84=== modified file 'docs/source/conf.py'
85--- docs/source/conf.py 2012-12-06 16:08:13 +0000
86+++ docs/source/conf.py 2013-04-08 19:17:22 +0000
87@@ -28,10 +28,13 @@
88 # All configuration values have a default; values that are commented out
89 # serve to show the default.
90
91-import sys
92+"""Provide documentation configuration."""
93+
94+
95+import itertools
96 import os
97-import itertools
98 import re
99+import sys
100
101 # If extensions (or modules to document with autodoc) are in another directory,
102 # add these directories to sys.path here. If the directory is relative to the
103@@ -41,10 +44,9 @@
104
105
106 class ModuleMock(object):
107- """
108- Mock class to avoid import errors when building the documentation in
109- readthedocs.org
110- """
111+
112+ """Mock class to avoid import errors."""
113+
114 def __init__(self, name):
115 self.__name__ = name
116
117@@ -69,10 +71,9 @@
118
119
120 class ClassMock(object):
121- """
122- Mock class to avoid module member access errors when building the
123- documentation in readthedocs.org
124- """
125+
126+ """Mock class to avoid import errors."""
127+
128 def __init__(self, *args, **kwargs):
129 pass
130
131@@ -84,8 +85,13 @@
132 sys.modules[mod_name] = ModuleMock(mod_name)
133
134
135-# Autogenerate help contents for manual pages when building manual pages
136 def get_parser_strings(script_name, parser):
137+ """Autogenerate help contents for manual pages when building them.
138+
139+ :returns: description, options, and epilog
140+ :rtype: tuple
141+
142+ """
143 description = parser.description
144
145 help_lines = parser.format_help().splitlines()
146@@ -98,9 +104,11 @@
147 options_lines.next() # Drop first line
148
149 def fix_line(line):
150- """
151- Remove indentation and replace curly braces with angle brackets
152- to match the expected format
153+ """Format argparse output.
154+
155+ Remove indentation and replace curly braces with angle brackets to
156+ match the expected format.
157+
158 """
159 line = line[2:]
160
161
162=== modified file 'examples/run_install_test.py'
163--- examples/run_install_test.py 2013-04-03 14:25:21 +0000
164+++ examples/run_install_test.py 2013-04-08 19:17:22 +0000
165@@ -1,4 +1,4 @@
166-#!/usr/bin/python
167+#!/usr/bin/env python
168
169 # Ubuntu Testing Automation Harness
170 # Copyright 2012 Canonical Ltd.
171@@ -15,6 +15,9 @@
172 # You should have received a copy of the GNU General Public License along
173 # with this program. If not, see <http://www.gnu.org/licenses/>.
174
175+"""Create a VM and run a test."""
176+
177+
178 import argparse
179 import logging
180 import sys
181
182=== modified file 'examples/run_test_bamboo_feeder.py'
183--- examples/run_test_bamboo_feeder.py 2013-04-03 14:25:21 +0000
184+++ examples/run_test_bamboo_feeder.py 2013-04-08 19:17:22 +0000
185@@ -1,4 +1,4 @@
186-#!/usr/bin/python
187+#!/usr/bin/env python
188
189 # Ubuntu Testing Automation Harness
190 # Copyright 2012 Canonical Ltd.
191@@ -15,6 +15,9 @@
192 # You should have received a copy of the GNU General Public License along
193 # with this program. If not, see <http://www.gnu.org/licenses/>.
194
195+"""Provision a panda board in a bamboo-feeder setup and run a test."""
196+
197+
198 import argparse
199 import os
200 import sys
201
202=== modified file 'examples/run_test_cobbler.py'
203--- examples/run_test_cobbler.py 2013-04-03 14:25:21 +0000
204+++ examples/run_test_cobbler.py 2013-04-08 19:17:22 +0000
205@@ -1,4 +1,4 @@
206-#!/usr/bin/python
207+#!/usr/bin/env python
208
209 # Ubuntu Testing Automation Harness
210 # Copyright 2012 Canonical Ltd.
211@@ -15,6 +15,9 @@
212 # You should have received a copy of the GNU General Public License along
213 # with this program. If not, see <http://www.gnu.org/licenses/>.
214
215+"""Provision a machine with cobbler and run a test."""
216+
217+
218 import argparse
219 import sys
220 import logging
221
222=== modified file 'examples/run_test_vm.py'
223--- examples/run_test_vm.py 2013-04-03 14:25:21 +0000
224+++ examples/run_test_vm.py 2013-04-08 19:17:22 +0000
225@@ -1,4 +1,4 @@
226-#!/usr/bin/python
227+#!/usr/bin/env python
228
229 # Ubuntu Testing Automation Harness
230 # Copyright 2012 Canonical Ltd.
231@@ -15,6 +15,9 @@
232 # You should have received a copy of the GNU General Public License along
233 # with this program. If not, see <http://www.gnu.org/licenses/>.
234
235+"""Create a VM and run a test."""
236+
237+
238 import argparse
239 import logging
240 import sys
241
242=== modified file 'examples/run_utah_tests.py'
243--- examples/run_utah_tests.py 2013-04-03 14:25:21 +0000
244+++ examples/run_utah_tests.py 2013-04-08 19:17:22 +0000
245@@ -1,4 +1,4 @@
246-#!/usr/bin/python
247+#!/usr/bin/env python
248
249 # Ubuntu Testing Automation Harness
250 # Copyright 2012 Canonical Ltd.
251@@ -15,6 +15,9 @@
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+"""Provision a machine a run a test."""
256+
257+
258 import argparse
259 import logging
260 import sys
261
262=== modified file 'tests/test_rsyslog.py'
263--- tests/test_rsyslog.py 2013-03-29 16:22:16 +0000
264+++ tests/test_rsyslog.py 2013-04-08 19:17:22 +0000
265@@ -13,6 +13,9 @@
266 # You should have received a copy of the GNU General Public License along
267 # with this program. If not, see <http://www.gnu.org/licenses/>.
268
269+"""Test syslog processing functionality."""
270+
271+
272 import logging
273 import socket
274 import time
275@@ -25,13 +28,17 @@
276
277
278 class TestRsyslog(unittest.TestCase):
279+
280+ """Test syslog processing code."""
281+
282 logger = logging.getLogger()
283 logger.setLevel(logging.DEBUG)
284 logging.getLogger('rsyslog').addHandler(logging.StreamHandler())
285
286 @staticmethod
287 def producer(port, messages):
288- print 'sending fake messages to port: %d' % port
289+ """Send messages to a port for testing."""
290+ print('sending fake messages to port: {}'.format(port))
291 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
292 s.connect(('localhost', port))
293 for m in messages:
294@@ -40,6 +47,7 @@
295
296 @staticmethod
297 def file_producer(f, messages, truncate=False):
298+ """Produce a file for testing."""
299 for m in messages:
300 f.write(m)
301 f.write('\n')
302@@ -52,9 +60,7 @@
303 truncate = False
304
305 def test_easy(self):
306- """
307- minimal test to make sure we can match a typical first message
308- """
309+ """Make sure we can match a typical first message."""
310 steps = [
311 {
312 "message": "test_easy",
313@@ -94,10 +100,7 @@
314 self.assertRaises(UTAHException, r.wait_for_install, steps)
315
316 def test_future(self):
317- """
318- test to make sure we can handle missing a message and understanding
319- where things are in the future
320- """
321+ """Test handling a missing message."""
322 steps = [
323 {
324 "message": "test_future",
325@@ -134,9 +137,7 @@
326 self.assertTrue(self.test_future_booted, 'booted callback not made')
327
328 def test_callbacks(self):
329- """
330- minimal test to make sure wait_for_steps works
331- """
332+ """Make sure wait_for_steps works."""
333 steps = [
334 {
335 "message": "test callbacks",
336@@ -182,9 +183,7 @@
337 self.assertTrue(self.test_callbacks_blah, 'blah callback not made')
338
339 def test_booted(self):
340- """
341- minimal test to make sure wait_for_booted works
342- """
343+ """Make sure wait_for_booted works."""
344 steps = [
345 {
346 "message": "test booted",
347@@ -203,9 +202,7 @@
348 r.wait_for_booted(steps)
349
350 def usefile(self, truncate):
351- """
352- minimal test to make sure wait_for_booted works when tailing a file
353- """
354+ """Make sure wait_for_booted works when tailing a file."""
355 steps = [
356 {
357 "message": "truncate test",
358@@ -229,7 +226,9 @@
359 r.wait_for_booted(steps)
360
361 def test_usefile(self):
362+ """Test tailing a file."""
363 self.usefile(False)
364
365 def test_usefile_truncation(self):
366+ """Test tailing a file with truncation."""
367 self.usefile(True)
368
369=== modified file 'utah-done.py'
370--- utah-done.py 2013-03-15 13:13:28 +0000
371+++ utah-done.py 2013-04-08 19:17:22 +0000
372@@ -10,6 +10,7 @@
373 - Fetch command failure
374 - Setup command failure
375 UNKNOWN: Unable to retrieve state file to check if client finished properly.
376+
377 """
378
379
380
381=== modified file 'utah/__init__.py'
382--- utah/__init__.py 2012-12-03 14:14:15 +0000
383+++ utah/__init__.py 2013-04-08 19:17:22 +0000
384@@ -13,6 +13,4 @@
385 # You should have received a copy of the GNU General Public License along
386 # with this program. If not, see <http://www.gnu.org/licenses/>.
387
388-"""
389-utah package
390-"""
391+"""utah"""
392
393=== modified file 'utah/cleanup.py'
394--- utah/cleanup.py 2013-04-03 14:25:21 +0000
395+++ utah/cleanup.py 2013-04-08 19:17:22 +0000
396@@ -15,6 +15,7 @@
397
398 """Generic functionality to execute callbacks on exit."""
399
400+
401 import atexit
402 import logging
403 import os
404@@ -67,7 +68,7 @@
405
406 """
407 timeout, command, args, kw = function
408- self.logger.debug('Running: ' +
409+ self.logger.debug('Running: %s',
410 commandstr(command, *args, **kw))
411 try:
412 utah.timeout.timeout(timeout, command, *args, **kw)
413@@ -102,7 +103,7 @@
414
415 """
416 if os.path.islink(path):
417- self.logger.debug('Removing link ' + path)
418+ self.logger.debug('Removing link %s', path)
419 os.unlink(path)
420 elif os.path.isfile(path):
421 self._clean_file(path)
422@@ -121,14 +122,14 @@
423 :type path: str
424
425 """
426- self.logger.debug('Changing permissions of ' + path)
427+ self.logger.debug('Changing permissions of %s', path)
428 try:
429 os.chmod(path, 0664)
430 except OSError as err:
431 self.logger.warning(
432 'OSError when changing file permissions: {}'
433 .format(str(err)))
434- self.logger.debug('Removing file ' + path)
435+ self.logger.debug('Removing file %s', path)
436 try:
437 os.unlink(path)
438 except OSError as err:
439
440=== modified file 'utah/client/__init__.py'
441--- utah/client/__init__.py 2012-12-03 14:31:27 +0000
442+++ utah/client/__init__.py 2013-04-08 19:17:22 +0000
443@@ -13,6 +13,4 @@
444 # You should have received a copy of the GNU General Public License along
445 # with this program. If not, see <http://www.gnu.org/licenses/>.
446
447-"""
448-utah.client
449-"""
450+"""utah.client"""
451
452=== modified file 'utah/client/battery.py'
453--- utah/client/battery.py 2013-03-13 12:13:12 +0000
454+++ utah/client/battery.py 2013-04-08 19:17:22 +0000
455@@ -1,11 +1,30 @@
456+# Ubuntu Testing Automation Harness
457+# Copyright 2013 Canonical Ltd.
458+
459+# This program is free software: you can redistribute it and/or modify it
460+# under the terms of the GNU General Public License version 3, as published
461+# by the Free Software Foundation.
462+
463+# This program is distributed in the hope that it will be useful, but
464+# WITHOUT ANY WARRANTY; without even the implied warranties of
465+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
466+# PURPOSE. See the GNU General Public License for more details.
467+
468+# You should have received a copy of the GNU General Public License along
469+# with this program. If not, see <http://www.gnu.org/licenses/>.
470+
471 """Helpers for battery measurements."""
472+
473+
474 import os
475 import re
476 import logging
477
478
479 class _Battery(object):
480+
481 """Battery information gathering."""
482+
483 POWER_SUPPLY_DIR = '/sys/class/power_supply'
484 BATTERY_DIR_REGEX = re.compile(r'bat.*', re.IGNORECASE)
485 BATTERY_FILE_REGEX = re.compile(r'(.*_now|capacity|status)')
486@@ -14,13 +33,14 @@
487 self.filenames = None
488
489 def get_data(self):
490- """Get data from the battery information files
491+ """Get data from the battery information files.
492
493 Every file is read and the contents of the file is written to a
494 dictionary using as key the basename of the file.
495
496 :returns: Data as read from the battery information files
497 :rtype: dict
498+
499 """
500 def read_file(filename):
501 """Read contents of a file and try to convert data to integer
502
503=== modified file 'utah/client/common.py'
504--- utah/client/common.py 2013-04-02 13:56:33 +0000
505+++ utah/client/common.py 2013-04-08 19:17:22 +0000
506@@ -15,25 +15,27 @@
507
508 """UTAH client common classes and functions."""
509
510+
511 import datetime
512-import re
513-import grp
514 import getpass
515-import json
516 import os
517 import platform
518 import pwd
519+import re
520 import signal
521 import subprocess
522+import time
523+
524+import jsonschema
525 import yaml
526-import jsonschema
527+
528+from utah.client.battery import battery
529
530 from utah.client.exceptions import (
531 YAMLParsingError,
532 YAMLEmptyFile,
533 )
534
535-from utah.client.battery import battery
536
537 CONFIG = {
538 'DEBUG': False,
539@@ -52,19 +54,29 @@
540 CLIENT_CONFIG = os.path.join(UTAH_DIR, 'config', 'client.json')
541 DEFAULT_STATE_FILE = os.path.join(UTAH_DIR, "state.yaml")
542
543-
544-# UTAH client return codes
545-# PASS: All test cases were executed and passed
546-# FAIL: All test cases were executed, but at least one of them failed
547-# ERROR: At least one error was detected that prevented a test case from being
548-# executed. Examples of situations that are considered an error are:
549-# - Fetch command failure
550-# - Setup command failure
551-# REBOOT: The system under test rebooted as required by a test case. To get
552-# final result the state file has to be checked.
553-# UNKNOWN: Unable to retrieve state file to check if client finished properly.
554-# INVALID_GROUP: The client was launched with a user other than root.
555+MEDIA_INFO = '/var/log/installer/media-info'
556+PRODUCT_UUID = '/sys/class/dmi/id/product_uuid'
557+
558+
559 class ReturnCodes:
560+
561+ """Provide consistent return codes for UTAH client.
562+
563+ PASS: All test cases were executed and passed
564+ FAIL: All test cases were executed, but at least one of them failed
565+ ERROR: At least one error was detected that prevented a test case from
566+ being executed. Examples of situations that are considered an error are:
567+ - Fetch command failure
568+ - Setup command failure
569+ REBOOT: The system under test rebooted as required by a test case. To get
570+ final result the state file has to be checked.
571+ UNKNOWN: Unable to retrieve state file to check if client finished
572+ properly.
573+ INVALID_USER: The client was launched with a user other than root.
574+ EXCEPTION_ERROR: An exception error was encountered.
575+
576+ """
577+
578 PASS = 0
579 FAIL = 1
580 ERROR = 2
581@@ -73,6 +85,7 @@
582 INVALID_USER = 5
583 EXCEPTION_ERROR = 6
584
585+
586 CMD_TC_BUILD = 'testcase_build'
587 CMD_TC_SETUP = 'testcase_setup'
588 CMD_TC_TEST = 'testcase_test'
589@@ -164,20 +177,22 @@
590
591 try:
592 stdout, stderr = p.communicate()
593- stdout = normalize_encoding(stdout)
594- stderr = normalize_encoding(stderr)
595+ stdout = _normalize_encoding(stdout)
596+ stderr = _normalize_encoding(stderr)
597
598 if timeout != 0:
599 signal.alarm(0)
600 except TimeoutAlarm:
601 pids = [p.pid]
602 # Kill p's children too.
603- pids.extend(get_process_children(p.pid))
604+ pids.extend(_get_process_children(p.pid))
605
606 for pid in pids:
607 # process might have died before getting to this line
608 # so wrap to avoid OSError: no such process
609 try:
610+ os.kill(pid, signal.SIGTERM)
611+ time.sleep(5)
612 os.kill(pid, signal.SIGKILL)
613 except OSError:
614 pass
615@@ -212,7 +227,7 @@
616 return make_result(**kwargs)
617
618
619-def normalize_encoding(value, encoding='utf-8'):
620+def _normalize_encoding(value, encoding='utf-8'):
621 """Normalize string encoding.
622
623 Make sure that byte strings are used only for ascii data and unicode
624@@ -226,7 +241,10 @@
625 :rtype: str
626
627 """
628- unicode_value = value.decode(encoding)
629+ try:
630+ unicode_value = value.decode(encoding)
631+ except UnicodeDecodeError:
632+ return value
633 try:
634 output = unicode_value.encode('ascii')
635 except UnicodeEncodeError:
636@@ -291,7 +309,7 @@
637 return res
638
639
640-def get_process_children(pid):
641+def _get_process_children(pid):
642 """Get the children processes of a given one.
643
644 :param pid: Process ID of the parent process
645@@ -300,11 +318,12 @@
646 :rtype: list(int)
647
648 """
649- p = subprocess.Popen('ps --no-headers -o pid --ppid %d' % pid, shell=True,
650- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
651- stdout, _stderr = p.communicate()
652-
653- return [int(pid) for pid in stdout.split()]
654+ try:
655+ pids = subprocess.check_output(['ps', '--no-headers', '-o', 'pid',
656+ '--ppid', str(pid)]).split()
657+ return [int(pid) for pid in pids]
658+ except subprocess.CalledProcessError:
659+ return []
660
661
662 def parse_yaml_file(filename):
663@@ -409,102 +428,6 @@
664 return debug
665
666
667-def raise_privileges(orig_privs):
668- """Raise privileges to original effective privileges.
669-
670- :param orig_privs: Original privileges
671- :type orig_privs: dict
672-
673- """
674- os.seteuid(orig_privs['euid'])
675- os.setegid(orig_privs['egid'])
676- os.setgroups(orig_privs['groups'])
677-
678-
679-def drop_privileges(user='nobody', group='nogroup', full=False):
680- """Change user and/or group to decrease privileges.
681-
682- Currently only drops effective privileges.
683-
684- :param user: User name to change to.
685- :type user: str
686- :param group: Group name to change to.
687- :type group: str
688-
689- :returns: Original prrivileges
690- :rtype: dict
691-
692- """
693-
694- orig_privs = {}
695-
696- orig_privs['uid'] = os.getuid()
697- orig_privs['euid'] = os.geteuid()
698- orig_privs['gid'] = os.getgid()
699- orig_privs['egid'] = os.getegid()
700- orig_privs['groups'] = os.getgroups()
701-
702- # If root, drop privileges
703- # inspired by: http://stackoverflow.com/a/2699996
704- if os.getuid() == 0 or os.geteuid() == 0:
705-
706- running_uid = pwd.getpwnam(user).pw_uid
707- running_gid = grp.getgrnam(group).gr_gid
708- os.setgroups([])
709-
710- if full:
711- os.setgid(running_gid)
712- else:
713- os.setegid(running_gid)
714-
715- if full:
716- os.setuid(running_uid)
717- else:
718- os.seteuid(running_uid)
719-
720- os.umask(077)
721-
722- return orig_privs
723-
724-
725-def gather_artifacts(extras=None, excludes=None):
726- """Print files contents, so that it's included in the logs.
727-
728- :param extras: Path to files to be included in the artifacts
729- :type extras: list(str)
730- :param excludes: Path to files to be exluded from the artifacts
731- :type excludes: list(str)
732-
733- """
734- if extras is None:
735- extras = []
736- if excludes is None:
737- excludes = []
738-
739- artifacts = ['/var/log/syslog', '/proc/cpuinfo']
740-
741- artifacts += extras
742-
743- # if excludes is a single item make it a list
744- if not isinstance(excludes, list):
745- excludes = [excludes]
746-
747- for exc in excludes:
748- try:
749- artifacts.remove(exc)
750- except ValueError:
751- print("{} is not already an artifact".format(exc))
752- pass
753-
754- for artifact in artifacts:
755- print("### Start {} ###".format(artifact))
756- with open(artifact, 'r') as fp:
757- for line in fp:
758- print(line.strip())
759- fp.close()
760- print("### End {} ###".format(artifact))
761-
762-
763 def get_media_info():
764 """Get the contents of the media-info file if available.
765
766@@ -516,13 +439,14 @@
767
768 """
769
770- filename = '/var/log/installer/media-info'
771-
772 media_info = 'unknown'
773
774- if os.path.exists(filename) and os.access(filename, os.R_OK):
775- with open(filename, 'r') as f:
776+ try:
777+ with open(MEDIA_INFO, 'r') as f:
778 media_info = f.read()
779+ except IOError:
780+ # If this fails, return the default
781+ pass
782
783 return media_info
784
785@@ -537,13 +461,14 @@
786
787 """
788
789- filename = '/sys/class/dmi/id/product_uuid'
790-
791 product_uuid = None
792
793- if os.path.exists(filename) and os.access(filename, os.R_OK):
794- with open(filename) as f:
795+ try:
796+ with open(PRODUCT_UUID) as f:
797 product_uuid = f.read().strip()
798+ except IOError:
799+ # If this fails, return the default
800+ pass
801
802 return product_uuid
803
804@@ -581,7 +506,8 @@
805 'i686': 'i386',
806 }
807
808- return arches.get(platform.machine(), 'unknown')
809+ arch = platform.machine()
810+ return arches.get(arch, arch)
811
812
813 def get_release():
814@@ -594,22 +520,6 @@
815 return platform.linux_distribution()[2]
816
817
818-def get_api_config():
819- """Parse the client configuration file for API configuration data.
820-
821- :returns: The value for the ``API`` field in the client configuration file.
822-
823- """
824-
825- data = {}
826- data['API'] = {}
827-
828- with open(CLIENT_CONFIG, 'r') as fp:
829- data = json.load(fp)
830-
831- return data['API']
832-
833-
834 def get_build_number():
835 """Get build number.
836
837@@ -624,152 +534,3 @@
838 match = pattern.match(host_info['media-info'])
839
840 return match.group(1) if match else '?'
841-
842-# VCS Support
843-# XXX: This maybe ought to be moved to another file
844-REPO_DEFAULT_DIR = "."
845-
846-
847-class VCSHandler(object):
848-
849- """Base class for Version Control System support.
850-
851- :param repo: Repository location
852- :type repo: str
853- :param destination:
854- Local directory where the repository should be made available
855- :type destination: str
856- :param battery_measurements:
857- Whether battery information should be gathered when running any of the
858- VCS commands
859- :type battery_measurements: bool
860-
861- """
862-
863- def __init__(self, repo, destination='', battery_measurements=False):
864- self.repo = repo
865- self.destination = destination
866- self.battery_measurements = battery_measurements
867-
868- def get(self, directory=REPO_DEFAULT_DIR):
869- """Execute VCS get command.
870-
871- The get command will make a copy of a given repository to the directory
872- specified as destination.
873-
874- :param directory: Working directory
875- :type directory: str
876- :returns: Get command execution result
877- :rtype: dict
878-
879- .. seealso:: :func:`run_cmd`
880-
881- """
882- return run_cmd(self.get_command,
883- cwd=directory,
884- cmd_type=CMD_TS_FETCH,
885- battery_measurements=self.battery_measurements)
886-
887- def revision(self, directory=REPO_DEFAULT_DIR):
888- """Execute revision command.
889-
890- :param directory: Working directory
891- :type directory: str
892- :returns: Revision command execution result.
893- :rtype: dict
894-
895- .. seealso:: :func:`run_cmd`, :meth:`get_revision`
896-
897- """
898- return run_cmd(self.rev_command,
899- cwd=directory,
900- cmd_type=CMD_TS_FETCH,
901- battery_measurements=self.battery_measurements)
902-
903- def get_revision(self, directory=REPO_DEFAULT_DIR):
904- """Print revision information.
905-
906- This is a wrapper around :meth:`revision` to make sure that the command
907- returned a success code.
908-
909- :param directory: Working directory
910- :type directory: str
911- :returns: Revision command returncode
912- :rtype: int
913-
914- .. seealso:: :meth:`revision`
915-
916- """
917- retval = None
918- result = self.revision(directory=directory)
919-
920- if result['returncode'] == 0:
921- retval = result['stdout'].strip()
922- else:
923- print(result)
924-
925- return retval
926-
927-
928-class BzrHandler(VCSHandler):
929-
930- """Bazaar VCS handler."""
931-
932- def __init__(self, repo, branch=True, options="", destination="",
933- **kwargs):
934- super(BzrHandler, self).__init__(repo, destination, **kwargs)
935- self.options = options
936-
937- if branch:
938- self.get_command = "bzr branch {} {} {}".format(
939- options,
940- self.repo,
941- self.destination,
942- )
943-
944- self.rev_command = "bzr revno"
945- else:
946- self.get_command = "bzr export {} {} {}".format(
947- options,
948- self.destination,
949- self.repo,
950- )
951-
952- self.rev_command = "bzr revno {}".format(self.repo)
953-
954-
955-class GitHandler(VCSHandler):
956-
957- """Git VCS handler."""
958-
959- def __init__(self, repo, options="", destination="", **kwargs):
960- super(GitHandler, self).__init__(repo, destination, **kwargs)
961- self.options = options
962-
963- self.get_command = "git clone {} {} {}".format(
964- options,
965- self.repo,
966- self.destination,
967- )
968-
969- self.rev_command = "git rev-parse HEAD"
970-
971-
972-class DevHandler(VCSHandler):
973-
974- """Development VCS handler.
975-
976- This isn't a handler for any VCS, but a helper to ease development and let
977- runlists point to a path that can be copied locally.
978-
979- """
980-
981- def __init__(self, repo, destination=".", **kwargs):
982- super(DevHandler, self).__init__(repo, destination, **kwargs)
983-
984- self.get_command = "cp -r {} {}".format(
985- self.repo,
986- self.destination,
987- )
988-
989- self.rev_command = "echo 'DEVELOPMENT'"
990
991=== modified file 'utah/client/examples/examples/test_one/test_one.py'
992--- utah/client/examples/examples/test_one/test_one.py 2012-03-27 21:22:53 +0000
993+++ utah/client/examples/examples/test_one/test_one.py 2013-04-08 19:17:22 +0000
994@@ -1,5 +1,8 @@
995 #!/usr/bin/env python
996
997+"""Basic example test script."""
998+
999+
1000 import time
1001
1002
1003
1004=== modified file 'utah/client/examples/examples/test_two/test_two.py'
1005--- utah/client/examples/examples/test_two/test_two.py 2012-03-27 15:58:56 +0000
1006+++ utah/client/examples/examples/test_two/test_two.py 2013-04-08 19:17:22 +0000
1007@@ -1,3 +1,6 @@
1008 #!/usr/bin/env python
1009
1010+"""An even more basic example test script."""
1011+
1012+
1013 print "test_two"
1014
1015=== modified file 'utah/client/examples/utah_tests/test_one/test_one.py'
1016--- utah/client/examples/utah_tests/test_one/test_one.py 2012-04-10 20:23:50 +0000
1017+++ utah/client/examples/utah_tests/test_one/test_one.py 2013-04-08 19:17:22 +0000
1018@@ -1,5 +1,8 @@
1019 #!/usr/bin/env python
1020
1021+"""Basic example test script."""
1022+
1023+
1024 import time
1025
1026
1027
1028=== modified file 'utah/client/examples/utah_tests/test_two/test_two.py'
1029--- utah/client/examples/utah_tests/test_two/test_two.py 2012-04-10 20:23:50 +0000
1030+++ utah/client/examples/utah_tests/test_two/test_two.py 2013-04-08 19:17:22 +0000
1031@@ -1,3 +1,6 @@
1032 #!/usr/bin/env python
1033
1034+"""An even more basic example test script."""
1035+
1036+
1037 print "test_two"
1038
1039=== modified file 'utah/client/examples/utah_tests_sample/sample_one/sample.py'
1040--- utah/client/examples/utah_tests_sample/sample_one/sample.py 2012-04-10 20:23:50 +0000
1041+++ utah/client/examples/utah_tests_sample/sample_one/sample.py 2013-04-08 19:17:22 +0000
1042@@ -1,3 +1,6 @@
1043 #!/usr/bin/env python
1044
1045+"""A very basic example test script."""
1046+
1047+
1048 print("sample_one test output goes here")
1049
1050=== modified file 'utah/client/exceptions.py'
1051--- utah/client/exceptions.py 2013-03-14 20:23:09 +0000
1052+++ utah/client/exceptions.py 2013-04-08 19:17:22 +0000
1053@@ -13,59 +13,70 @@
1054 # You should have received a copy of the GNU General Public License along
1055 # with this program. If not, see <http://www.gnu.org/licenses/>.
1056
1057-"""
1058-UTAH client exceptions
1059-"""
1060+"""Provide UTAH client exceptions."""
1061
1062
1063 class UTAHClientError(Exception):
1064- """
1065- Base class of all exceptions in the client
1066- """
1067+
1068+ """Base class of all exceptions in the client."""
1069+
1070+ pass
1071
1072
1073 class BadDir(UTAHClientError):
1074- """
1075+
1076+ """Directory error.
1077+
1078 Raised when some test directory isn't found
1079 or an error happens when trying to change to it
1080+
1081 """
1082
1083+ pass
1084+
1085
1086 class MissingFile(UTAHClientError):
1087- """
1088- Raised when yaml file with metadata about tests
1089- cannot be found
1090- """
1091+
1092+ """Raised when yaml file with metadata about tests cannot be found."""
1093+
1094+ pass
1095
1096
1097 class BadMasterRunlist(UTAHClientError):
1098- """
1099- Raised when master runlist isn't in the expected format
1100- """
1101+
1102+ """Raised when master runlist isn't in the expected format."""
1103+
1104+ pass
1105
1106
1107 class YAMLParsingError(UTAHClientError):
1108- """
1109+
1110+ """Provide more detailed yaml.load exception handling.
1111+
1112 Used to provide the filename and the location
1113 in which the parsing error happened when calling yaml.load
1114+
1115 """
1116
1117+ pass
1118+
1119
1120 class YAMLEmptyFile(UTAHClientError):
1121- """
1122- Used to signal that a file that was supposed to contain yaml data
1123- is actually empty
1124- """
1125+
1126+ """Raised when a file that was supposed to contain yaml data is empty."""
1127+
1128+ pass
1129
1130
1131 class ValidationError(UTAHClientError):
1132- """
1133- Used to provide additional information when schema validation fails
1134- """
1135+
1136+ """Used to provide additional information when schema validation fails."""
1137+
1138+ pass
1139
1140
1141 class MissingData(UTAHClientError):
1142- """
1143- Raised when there is missing data that is required for an object
1144- to proceed.
1145- """
1146+
1147+ """Raised when there is missing data required for an object to proceed."""
1148+
1149+ pass
1150
1151=== modified file 'utah/client/phoenix.py'
1152--- utah/client/phoenix.py 2012-12-11 15:18:01 +0000
1153+++ utah/client/phoenix.py 2013-04-08 19:17:22 +0000
1154@@ -15,7 +15,8 @@
1155 # You should have received a copy of the GNU General Public License along
1156 # with this program. If not, see <http://www.gnu.org/licenses/>.
1157
1158-# phoenix.py - A bootstrap script for testsuite/testcase authors
1159+"""phoenix.py - A bootstrap script for testsuite/testcase authors"""
1160+
1161
1162 import argparse
1163 import os
1164
1165=== modified file 'utah/client/result.py'
1166--- utah/client/result.py 2013-03-01 13:04:22 +0000
1167+++ utah/client/result.py 2013-04-08 19:17:22 +0000
1168@@ -13,11 +13,14 @@
1169 # You should have received a copy of the GNU General Public License along
1170 # with this program. If not, see <http://www.gnu.org/licenses/>.
1171
1172+"""Provide functionality for test result handling."""
1173+
1174+
1175 import json
1176 import sys
1177 import yaml
1178
1179-from .common import (
1180+from utah.client.common import (
1181 get_host_info,
1182 get_build_number,
1183 get_release,
1184@@ -25,41 +28,10 @@
1185 )
1186
1187
1188-def get_smoke_data(_result):
1189- data = {}
1190- data['build_no'] = get_build_number()
1191-
1192- return data
1193-
1194-
1195-def get_kernel_sru_data(_result):
1196- data = {}
1197-
1198- return data
1199-
1200-
1201-# XXX: this logic should probably be moved into the __init__ method
1202-# of the result object since there will be one result object per
1203-# master.run
1204-def create_payload(result):
1205- publish_type = result.publish_type
1206-
1207- if publish_type is None:
1208- return None
1209-
1210- type_map = {
1211- 'smoke': get_smoke_data,
1212- 'kernel-sru': get_kernel_sru_data,
1213- }
1214-
1215- if publish_type and publish_type in type_map.iterkeys():
1216- return type_map[publish_type](result)
1217-
1218-
1219 class Result(object):
1220- """
1221- Result collection class.
1222- """
1223+
1224+ """Result collection class."""
1225+
1226 def __init__(self, name=None, testsuite=None, testcase=None,
1227 runlist=None, publish_type=None, install_type=None):
1228 self.results = []
1229@@ -85,14 +57,13 @@
1230 :param old_results: Old results as loaded from the report file
1231 :type old_results: str
1232
1233- .. seealso:: :meth:`payload`
1234+ .. seealso:: :meth:`_payload`
1235
1236 """
1237 raise NotImplementedError
1238
1239 def add_result(self, result, extra_info=None):
1240- """
1241- Add a result to the object.
1242+ """Add a result to the object.
1243
1244 Note: 'result' is expected to be a dictionary like this::
1245
1246@@ -104,6 +75,7 @@
1247 'start_time': '',
1248 'time_delta': '',
1249 }
1250+
1251 """
1252 if result is None:
1253 return
1254@@ -127,8 +99,13 @@
1255 self.status = 'FAIL'
1256
1257 def result(self, verbose=False):
1258- """
1259- Output a text based result.
1260+ """Output a text based result.
1261+
1262+ :param verbose: Enable verbose mode
1263+ :type verbose: bool
1264+ :returns: Status 'PASS' 'FAIL' or 'ERROR'
1265+ :rtype: str
1266+
1267 """
1268 status = self.status
1269 sep = '-' * 70
1270@@ -140,19 +117,19 @@
1271 print sep
1272
1273 for result in self.results:
1274- print "command: %s" % result['command']
1275- print "returned: %d" % result['returncode']
1276- print "started: %s" % result['start_time']
1277- print "runtime: %s" % result['time_delta']
1278+ print 'command: {}'.format(result['command'])
1279+ print 'returned: {}'.format(str(result['returncode']))
1280+ print 'started: {}'.format(result['start_time'])
1281+ print 'runtime: {}'.format(result['time_delta'])
1282
1283 if self.testsuite is not None:
1284- print "testsuite: %s" % self.testsuite
1285+ print "testsuite: {}".format(self.testsuite)
1286
1287 if self.testcase is not None:
1288- print "testcase: %s" % self.testcase
1289+ print "testcase: {}".format(self.testcase)
1290
1291 if self.runlist is not None:
1292- print "runlist: %s" % self.runlist
1293+ print "runlist: {}".format(self.runlist)
1294
1295 if self.status != 'PASS' or verbose and result['stdout'] != '':
1296 print "stdout: "
1297@@ -164,37 +141,33 @@
1298
1299 print
1300
1301- data = self.payload()
1302+ data = self._payload()
1303
1304 for key, value in data.iteritems():
1305 print '{}: {}'.format(key, value)
1306
1307- self.clear_results()
1308+ self._clear_results()
1309
1310 return status
1311
1312- def clear_results(self):
1313- # reset the list of results so this object can be reused.
1314+ def _clear_results(self):
1315+ """Reset the list of results so this object can be reused."""
1316 self.results = []
1317
1318 # reset the status, this should be safe since the only places that
1319- # should be calling clear_results() is testcase.run(), testsuite.run(),
1320- # and runner after attempting to fetch the testsuite.
1321+ # should be calling _clear_results() is testcase.run(),
1322+ # testsuite.run(), and runner after attempting to fetch the testsuite.
1323 self.status = 'PASS'
1324
1325- def count_results(self):
1326+ def _count_results(self):
1327 return len(self.results)
1328
1329- def publish_results(self):
1330- # TBD: Use url and token
1331- # url = self.publish_url
1332- # token = self.publish_token
1333-
1334- create_payload(self)
1335-
1336- def payload(self):
1337- """
1338- Construct the result payload.
1339+ def _payload(self):
1340+ """Construct the result payload.
1341+
1342+ :returns: Test result
1343+ :rtype: dict
1344+
1345 """
1346 host_info = get_host_info()
1347 data = {
1348@@ -229,27 +202,35 @@
1349
1350 # Trick to get strings printed as literal blocks
1351 # inspired by: http://stackoverflow.com/a/7445560/183066
1352-class LiteralString(object):
1353+class _LiteralString(object):
1354 def __init__(self, str_data):
1355 self.str_data = str_data
1356
1357
1358-def literal_block(dumper, data):
1359+def _literal_block(dumper, data):
1360 return dumper.represent_scalar('tag:yaml.org,2002:str',
1361 data.str_data, style='|')
1362-yaml.add_representer(LiteralString, literal_block)
1363+yaml.add_representer(_LiteralString, _literal_block)
1364
1365
1366 class ResultYAML(Result):
1367+
1368+ """Return results in a YAML format."""
1369+
1370 def _literalize(self, data):
1371- """
1372- Transform long strings into literal blocks
1373+ """Transform long strings into literal blocks.
1374+
1375+ :param data: Data to literalize
1376+ :type data: string, dict, or list
1377+ :returns: literalized form of data
1378+ :rtype: str, dict, or list
1379+
1380 """
1381 if isinstance(data, basestring) and '\n' in data:
1382 # Remove trailing whitespace to serialize in yaml
1383 # as a literal string
1384 lines = [line.rstrip() for line in data.splitlines()]
1385- return LiteralString('\n'.join(lines))
1386+ return _LiteralString('\n'.join(lines))
1387
1388 if isinstance(data, dict):
1389 for key, value in data.iteritems():
1390@@ -259,16 +240,22 @@
1391 for element in data]
1392 return data
1393
1394- def result(self, verbose=False):
1395+ def result(self, _verbose=False):
1396+ """Output a YAML result.
1397+
1398+ :returns: Status 'PASS' 'FAIL' or 'ERROR'
1399+ :rtype: str
1400+
1401+ """
1402 if self.results:
1403- data = self._literalize(self.payload())
1404+ data = self._literalize(self._payload())
1405 yaml.dump(data, sys.stdout,
1406 explicit_start='---',
1407 default_flow_style=False,
1408 allow_unicode=True)
1409
1410 status = self.status
1411- self.clear_results()
1412+ self._clear_results()
1413
1414 return status
1415
1416@@ -278,7 +265,7 @@
1417 :param old_results: Old results as loaded from the report file
1418 :type old_results: str
1419
1420- .. seealso:: :meth:`payload`
1421+ .. seealso:: :meth:`_payload`
1422
1423 """
1424 data = yaml.load(old_results)
1425@@ -293,13 +280,21 @@
1426
1427
1428 class ResultJSON(Result):
1429- def result(self, verbose=False):
1430-
1431+
1432+ """Return results in a JSON format."""
1433+
1434+ def result(self, _verbose=False):
1435+ """Output a YAML result.
1436+
1437+ :returns: Status 'PASS' 'FAIL' or 'ERROR'
1438+ :rtype: str
1439+
1440+ """
1441 if self.results:
1442- data = self.payload()
1443+ data = self._payload()
1444 json.dump(data, sys.stdout, indent=4)
1445
1446 status = self.status
1447- self.clear_results()
1448+ self._clear_results()
1449
1450 return status
1451
1452=== modified file 'utah/client/runner.py'
1453--- utah/client/runner.py 2013-03-21 15:43:29 +0000
1454+++ utah/client/runner.py 2013-04-08 19:17:22 +0000
1455@@ -13,6 +13,8 @@
1456 # You should have received a copy of the GNU General Public License along
1457 # with this program. If not, see <http://www.gnu.org/licenses/>.
1458
1459+"""Provide code to actually run the tests."""
1460+
1461
1462 import datetime
1463 import jsonschema
1464@@ -25,14 +27,11 @@
1465 from utah.client import exceptions
1466 from utah.client.battery import battery
1467 from utah.client.common import (
1468- BzrHandler,
1469 CMD_TC_REBOOT,
1470 DATE_FORMAT,
1471 DEFAULT_STATE_FILE,
1472 DEFAULT_TSLIST,
1473 DefaultValidator,
1474- DevHandler,
1475- GitHandler,
1476 make_result,
1477 MASTER_RUNLIST,
1478 parse_yaml_file,
1479@@ -42,12 +41,17 @@
1480 from utah.client.result import Result
1481 from utah.client.state_agent import StateAgentYAML
1482 from utah.client.testsuite import TestSuite
1483+from utah.client.vcs import (
1484+ BzrHandler,
1485+ DevHandler,
1486+ GitHandler,
1487+)
1488 from utah.exceptions import UTAHException
1489 from utah.retry import retry
1490 from utah.timeout import timeout
1491
1492 RC_LOCAL = '/etc/rc.local'
1493-RC_LOCAL_BACKUP = '%s-utah.bak' % RC_LOCAL
1494+RC_LOCAL_BACKUP = '{}-utah.bak'.format(RC_LOCAL)
1495
1496 # XXX: need to output to the same file that was supplied on the original
1497 # run.
1498@@ -58,10 +62,11 @@
1499
1500
1501 class Runner(object):
1502- """
1503- The main runner.
1504+
1505+ """Provide The main runner class.
1506
1507 Parses a master runlist, builds a list of TestSuites, and runs them.
1508+
1509 """
1510
1511 status = "NOTRUN"
1512@@ -216,10 +221,7 @@
1513 self.result.name = self.name
1514
1515 def backup_rc_local(self):
1516- """
1517- Backup /etc/rc.local if it exists.
1518- """
1519-
1520+ """Backup /etc/rc.local if it exists."""
1521 # Ignore permission denied errors since we only care if a reboot is
1522 # pending whether or not we can write to RC_LOCAL(_BACKUP).
1523 try:
1524@@ -230,11 +232,7 @@
1525 'Failed to backup rc.local: {}'.format(e))
1526
1527 def reset_rc_local(self):
1528- """
1529- Restore original /etc/rc.local if there is a backup
1530- Otherwise remove it.
1531- """
1532-
1533+ """Restore /etc/rc.local if there is a backup, remove it otherwise."""
1534 # Ignore permission denied errors since we only care if a reboot is
1535 # pending whether or not we can write to RC_LOCAL(_BACKUP).
1536 try:
1537@@ -248,9 +246,7 @@
1538 'Failed to backup rc.local: {}'.format(e))
1539
1540 def setup_rc_local(self, rc_local=RC_LOCAL, runlist=None):
1541- """
1542- Setup /etc/rc.local to kick-off a --resume on successful boot.
1543- """
1544+ """Setup /etc/rc.local to kick-off a --resume on successful boot."""
1545 runlist = runlist or self.master_runlist or MASTER_RUNLIST
1546
1547 self.rc_local_content = rc_local_content
1548@@ -265,6 +261,12 @@
1549 'Error setting up rc.local: {}'.format(err))
1550
1551 def process_results(self):
1552+ """Add stats to results and process them.
1553+
1554+ :returns: A return code based on the test status.
1555+ :rtype: int
1556+
1557+ """
1558 # Add stats to the results
1559 self.result.failures = self.failures
1560 self.result.passes = self.passes
1561@@ -276,8 +278,13 @@
1562 return self.returncode()
1563
1564 def run(self):
1565- """
1566- Run the test suites we've parsed.
1567+ """Run the test suites we've parsed.
1568+
1569+ :returns: The result of process_results, which is a return code.
1570+ :rtype: int
1571+
1572+ .. seealso:: :meth:`process_results`
1573+
1574 """
1575 if self.battery_measurements:
1576 self.result.start_battery = battery.get_data()
1577@@ -315,12 +322,11 @@
1578 return self.process_results()
1579
1580 def add_suite(self, suite):
1581+ """Add a test suite to run."""
1582 self.suites.append(suite)
1583
1584 def get_fetched_suites(self):
1585- """
1586- Return a list of fetched suites from the state_agent.
1587- """
1588+ """Return a list of fetched suites from the state_agent."""
1589 state = self.state_agent.load_state()
1590
1591 fetched_suites = []
1592@@ -331,6 +337,7 @@
1593 return fetched_suites
1594
1595 def load_state(self):
1596+ """Load the state saved by a previous partial run (i.e., a reboot)."""
1597 state = self.state_agent.load_state()
1598
1599 self.master_runlist = state['master_runlist']
1600@@ -351,10 +358,12 @@
1601 self.fetched_suites = state['fetched_suites']
1602
1603 def save_state(self):
1604- """
1605- Save the list of tests we are to run and whether we've run them.
1606- """
1607-
1608+ """Save the list of tests we are to run and whether we've run them.
1609+
1610+ :returns: state of currently run tests
1611+ :rtype: dict
1612+
1613+ """
1614 self.backup_runlist = os.path.join(self.testdir, 'master.run-reboot')
1615
1616 if (os.path.exists(
1617@@ -390,9 +399,11 @@
1618 return state
1619
1620 def count_suites(self):
1621+ """Return the number of test suites in the runner."""
1622 return len(self.suites)
1623
1624 def count_tests(self):
1625+ """Return the number of test cases in the runner."""
1626 tests = 0
1627
1628 for suite in self.suites:
1629@@ -401,10 +412,7 @@
1630 return tests
1631
1632 def process_master_runlist(self, runlist=None, resume=False):
1633- """
1634- Parse a master runlist building a list of suites from the parsed data.
1635-
1636- """
1637+ """Parse a master runlist and build a list of suites from the data."""
1638 runlist = runlist or self.master_runlist
1639
1640 # Download runlist using the URL passed through the commmand line to
1641@@ -511,6 +519,7 @@
1642 except OSError as err:
1643 raise exceptions.BadDir(err)
1644
1645+ # TODO: move this somewhere else
1646 def vcs_get_retriable():
1647 result = vcs_handler.get(directory=name)
1648 if (isinstance(vcs_handler, BzrHandler) and
1649@@ -555,12 +564,11 @@
1650 self.add_suite(s)
1651
1652 def get_next_suite(self):
1653- """
1654- Return the next suite to be run.
1655+ """Return the next suite to be run.
1656
1657 Mainly used for debugging.
1658+
1659 """
1660-
1661 suite = None
1662
1663 for s in self.suites:
1664@@ -571,18 +579,18 @@
1665 return suite
1666
1667 def get_next_test(self):
1668- """
1669- Return the next test to be run.
1670+ """Return the next test to be run.
1671
1672 Mainly used for debugging.
1673+
1674 """
1675 return self.get_next_suite().get_next_test()
1676
1677 def reboot(self):
1678- """
1679- Reboot the machine.
1680+ """Reboot the machine.
1681
1682 Save state, setup /etc/rc.local, and shutdown.
1683+
1684 """
1685 # Create fake result for logging purposes
1686 command = 'shutdown -r now'
1687@@ -613,25 +621,10 @@
1688 # End of execution
1689
1690 def returncode(self):
1691+ """Provide return code based on test status."""
1692 if self.errors > 0 or self.fetch_errors > 0:
1693 return ReturnCodes.ERROR
1694 elif self.failures > 0:
1695 return ReturnCodes.FAIL
1696 else:
1697 return ReturnCodes.PASS
1698-
1699- def report(self):
1700- tests_run = self.passes + self.errors + self.failures
1701-
1702- output = ("total: %d, passes: %d, failure: %d, error: %d"
1703- % (tests_run, self.passes, self.failures,
1704- self.errors + self.fetch_errors))
1705-
1706- if self.errors > 0 or self.fetch_errors > 0:
1707- result = "ERROR"
1708- elif self.failures > 0:
1709- result = "FAIL"
1710- else:
1711- result = "PASS"
1712-
1713- return "%s - %s" % (result, output)
1714
1715=== modified file 'utah/client/state_agent.py'
1716--- utah/client/state_agent.py 2012-12-03 14:02:18 +0000
1717+++ utah/client/state_agent.py 2013-04-08 19:17:22 +0000
1718@@ -13,17 +13,22 @@
1719 # You should have received a copy of the GNU General Public License along
1720 # with this program. If not, see <http://www.gnu.org/licenses/>.
1721
1722+"""Provide functionality for saving and restoring run state."""
1723+
1724+
1725 import os
1726+
1727 import yaml
1728
1729 from utah.client.common import DEFAULT_STATE_FILE, parse_yaml_file
1730
1731
1732 class StateAgent(object):
1733- """
1734- State saving base class.
1735+
1736+ """State saving base class.
1737
1738 Accepts a dictionary of state info and prints it to STDOUT.
1739+
1740 """
1741
1742 state_file = DEFAULT_STATE_FILE
1743@@ -33,9 +38,7 @@
1744 self.state_file = state_file
1745
1746 def clean(self):
1747- """
1748- Clean up the state file if it exists.
1749- """
1750+ """Clean up the state file if it exists."""
1751 # Don't fail if the state_file doesn't exists.
1752 try:
1753 os.remove(self.state_file)
1754@@ -44,17 +47,17 @@
1755 pass
1756
1757 def save_state(self, state):
1758- """
1759- Save state to the state_file.
1760- """
1761-
1762+ """Save state to the state_file."""
1763 fp = open(self.state_file, 'w')
1764 fp.write(str(state))
1765 fp.close()
1766
1767 def load_state(self):
1768- """
1769- Load state from the state_file.
1770+ """Load state from the state_file.
1771+
1772+ :returns: state information from file.
1773+ :rtype: dict
1774+
1775 """
1776 state = {}
1777
1778@@ -67,22 +70,21 @@
1779
1780
1781 class StateAgentYAML(StateAgent):
1782- """
1783- YAML based state saver.
1784- """
1785+
1786+ """YAML based state saver."""
1787
1788 def save_state(self, state):
1789- """
1790- Output the state as YAML.
1791- """
1792+ """Output the state as YAML."""
1793 yaml_state = yaml.dump(state, default_flow_style=False)
1794 super(StateAgentYAML, self).save_state(yaml_state)
1795
1796 def load_state(self):
1797- """
1798- Load state from YAML state_file.
1799- """
1800-
1801+ """Load state from YAML state_file.
1802+
1803+ :returns: state information from YAML file
1804+ :rtype: dict
1805+
1806+ """
1807 state = {}
1808
1809 if os.path.exists(self.state_file):
1810
1811=== modified file 'utah/client/testcase.py'
1812--- utah/client/testcase.py 2013-03-14 20:53:20 +0000
1813+++ utah/client/testcase.py 2013-04-08 19:17:22 +0000
1814@@ -13,35 +13,35 @@
1815 # You should have received a copy of the GNU General Public License along
1816 # with this program. If not, see <http://www.gnu.org/licenses/>.
1817
1818-"""
1819-Testcase specific code.
1820-"""
1821-
1822+"""Testcase specific code."""
1823+
1824+
1825+import os
1826
1827 import jsonschema
1828-import os
1829
1830 from utah.client.common import (
1831- run_cmd,
1832- parse_control_file,
1833- do_nothing,
1834 CMD_TC_BUILD,
1835+ CMD_TC_CLEANUP,
1836 CMD_TC_SETUP,
1837 CMD_TC_TEST,
1838- CMD_TC_CLEANUP,
1839+ do_nothing,
1840+ parse_control_file,
1841+ run_cmd,
1842 )
1843 from utah.client.exceptions import (
1844+ MissingData,
1845 MissingFile,
1846 ValidationError,
1847- MissingData,
1848 )
1849
1850
1851 class TestCase(object):
1852- """
1853- The TestCase class.
1854+
1855+ """Base class describing a test case.
1856
1857 status is one of 'NOTRUN', 'BUILD', 'SETUP', 'RUN', 'CLEANUP', or 'DONE'
1858+
1859 """
1860
1861 status = 'NOTRUN'
1862@@ -100,15 +100,15 @@
1863 battery_measurements=False,
1864 _control_data=None, _save_state_callback=None,
1865 _reboot_callback=None):
1866- """
1867- Build a TestCase from a control file's data.
1868+ """Build a TestCase from a control file's data.
1869
1870 'name' is a directory where the test case resides.
1871+
1872 """
1873 self.name = name
1874 self.path = path
1875 self.working_dir = path
1876- self.filename = "%s/tc_control" % path
1877+ self.filename = "{}/tc_control".format(path)
1878 self.results = []
1879 self.result = result
1880 self.timeout = timeout
1881@@ -158,20 +158,16 @@
1882 self.command = command
1883
1884 def __str__(self):
1885- return ("%s: %s, %s, %s"
1886- % (self.name, self.description, self.command, self.timeout))
1887+ return ("{}: {}, {}, {}".format(
1888+ self.name, self.description, self.command, self.timeout))
1889
1890 def set_status(self, status):
1891- """
1892- Set the status for the test case and call the save state callback.
1893- """
1894+ """Set the status and call the save state callback."""
1895 self.status = status
1896 self.save_state_callback()
1897
1898 def build(self, result):
1899- """
1900- Run build, but only if we haven't started a run yet.
1901- """
1902+ """Run build, but only if we haven't started a run yet."""
1903 if self.status == 'NOTRUN':
1904 self.set_status('BUILD')
1905 cmd_result = run_cmd(
1906@@ -182,9 +178,7 @@
1907 result.add_result(cmd_result)
1908
1909 def setup(self, result):
1910- """
1911- Run tc_setup, but only if build() has just passed.
1912- """
1913+ """Run tc_setup, but only if build() has just passed."""
1914 if self.status == 'BUILD' and result.status == 'PASS':
1915 self.set_status('SETUP')
1916 cmd_result = run_cmd(
1917@@ -195,9 +189,7 @@
1918 result.add_result(cmd_result)
1919
1920 def cleanup(self, result):
1921- """
1922- Run tc_cleanup after a run.
1923- """
1924+ """Run tc_cleanup after a run."""
1925 if self.status == 'RUN':
1926 self.set_status('CLEANUP')
1927 cmd_result = run_cmd(
1928@@ -208,10 +200,14 @@
1929 result.add_result(cmd_result)
1930
1931 def run(self):
1932- """
1933- Run the test case; including any build, setup, and cleanup commands.
1934- """
1935-
1936+ """Run the complete test case.
1937+
1938+ This includes any build, setup, and cleanup commands.
1939+
1940+ :returns: Whether to keep running tests (True) or reboot (False)
1941+ :rtype: bool
1942+
1943+ """
1944 if self.is_done():
1945 return 'PASS'
1946
1947@@ -280,9 +276,8 @@
1948
1949 need_reboot = False
1950 if (
1951- self.reboot == 'always'
1952- or self.reboot == 'pass'
1953- and result.status == 'PASS'
1954+ self.reboot == 'always' or
1955+ (self.reboot == 'pass' and result.status == 'PASS')
1956 ):
1957 need_reboot = True
1958
1959@@ -299,25 +294,21 @@
1960 return keep_going
1961
1962 def process_overrides(self, overrides):
1963- """
1964- Sets override values from a TestSuite runlist for this test case.
1965- """
1966+ """Set override values from a TestSuite runlist for this test case."""
1967 for key, value in overrides.iteritems():
1968 setattr(self, key, value)
1969
1970 def load_state(self, state):
1971- """
1972- Restore state from the supplied dictionary.
1973+ """Restore state from the supplied dictionary.
1974
1975 Requires that 'state' has the same fieldnames as the TestCase class.
1976+
1977 """
1978 for key, value in state.iteritems():
1979 setattr(self, key, value)
1980
1981 def save_state(self):
1982- """
1983- Returns a dictionary representing the test's state.
1984- """
1985+ """Return a dictionary representing the test's state."""
1986 state = {
1987 'name': self.name,
1988 'path': self.path,
1989@@ -338,9 +329,13 @@
1990 return state
1991
1992 def is_done(self):
1993- """
1994- Determine if the case is done. This might mean that something has
1995- failed. Used by suite to determine if the suite needs to be re-run
1996- on resume.
1997+ """Determine if the case is done.
1998+
1999+ This might mean that something has failed.
2000+ Used by suite to determine if the suite needs to be re-run on resume.
2001+
2002+ :returns: Whether the test case is finished (done or cleaned up)
2003+ :rtype: bool
2004+
2005 """
2006 return self.status == 'DONE' or self.status == 'CLEANUP'
2007
2008=== modified file 'utah/client/tests/__init__.py'
2009--- utah/client/tests/__init__.py 2012-12-08 02:10:12 +0000
2010+++ utah/client/tests/__init__.py 2013-04-08 19:17:22 +0000
2011@@ -12,3 +12,5 @@
2012
2013 # You should have received a copy of the GNU General Public License along
2014 # with this program. If not, see <http://www.gnu.org/licenses/>.
2015+
2016+"""utah.client.tests"""
2017
2018=== modified file 'utah/client/tests/common.py'
2019--- utah/client/tests/common.py 2013-03-21 20:46:21 +0000
2020+++ utah/client/tests/common.py 2013-04-08 19:17:22 +0000
2021@@ -13,6 +13,9 @@
2022 # You should have received a copy of the GNU General Public License along
2023 # with this program. If not, see <http://www.gnu.org/licenses/>.
2024
2025+"""Provide common functions and content for self tests."""
2026+
2027+
2028 import os
2029 import shutil
2030
2031@@ -20,8 +23,7 @@
2032
2033
2034 def get_module_path():
2035- """
2036- Return the path for the 'utah' directory.
2037+ """Return the path for the 'utah' directory.
2038
2039 This is used in tests to refer to the location where the branch resides.
2040 It is used in conjunction with running the self tests via:
2041@@ -29,6 +31,7 @@
2042 sudo nosetests utah/client/tests
2043
2044 from the top-level directory in the repo.
2045+
2046 """
2047 import utah
2048
2049@@ -38,6 +41,7 @@
2050
2051 return module_path
2052
2053+
2054 master_runlist_content = """# utah/self_test.py master runlist
2055 # needed for utah/self_test.py runs
2056 ---
2057@@ -64,11 +68,12 @@
2058
2059
2060 def setUp():
2061- """
2062- Set up a master.run file that will copy our 'examples' directory to the
2063- testing directory (usually /var/lib/utah).
2064- """
2065-
2066+ """Create a sample master.run file.
2067+
2068+ This file will copy our 'examples' directory to the testing directory
2069+ (usually /var/lib/utah).
2070+
2071+ """
2072 # If we're not root don't bother
2073 if os.geteuid() != 0 and os.getuid() != 0:
2074 raise RuntimeError('These tests must be run as root')
2075@@ -92,10 +97,7 @@
2076
2077
2078 def tearDown():
2079- """
2080- Cleanup after ourselves.
2081- """
2082-
2083+ """Clean up after ourselves."""
2084 # restore the copy of the master runlist
2085 if os.path.exists(master_runlist_bak):
2086 os.rename(master_runlist_bak, master_runlist)
2087@@ -107,10 +109,12 @@
2088
2089
2090 def _get_partial_state_file(filename):
2091- """
2092- Read a partial state file from disk.
2093- """
2094-
2095+ """Read a partial state file from disk.
2096+
2097+ :returns: A pre-defined partially run test suite state.
2098+ :rtype: dict
2099+
2100+ """
2101 state = None
2102
2103 with open(filename, 'r') as fp:
2104
2105=== removed file 'utah/client/tests/manual_privileges.py'
2106--- utah/client/tests/manual_privileges.py 2012-12-08 02:10:12 +0000
2107+++ utah/client/tests/manual_privileges.py 1970-01-01 00:00:00 +0000
2108@@ -1,188 +0,0 @@
2109-# Ubuntu Testing Automation Harness
2110-# Copyright 2012 Canonical Ltd.
2111-
2112-# This program is free software: you can redistribute it and/or modify it
2113-# under the terms of the GNU General Public License version 3, as published
2114-# by the Free Software Foundation.
2115-
2116-# This program is distributed in the hope that it will be useful, but
2117-# WITHOUT ANY WARRANTY; without even the implied warranties of
2118-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2119-# PURPOSE. See the GNU General Public License for more details.
2120-
2121-# You should have received a copy of the GNU General Public License along
2122-# with this program. If not, see <http://www.gnu.org/licenses/>.
2123-
2124-######################################################################
2125-### Privileges Tests
2126-######################################################################
2127-
2128-import os
2129-import pwd
2130-import grp
2131-import shlex
2132-import subprocess
2133-import unittest
2134-
2135-from utah.client.common import drop_privileges, raise_privileges
2136-
2137-NOBODY_UID = pwd.getpwnam('nobody').pw_uid
2138-NOGROUP_GID = grp.getgrnam('nogroup').gr_gid
2139-
2140-TMP_CONTENT = """Some data"""
2141-TMP_FILENAME = '/tmp/priv_test.tmp'
2142-
2143-
2144-def print_priv(msg=None):
2145- """
2146- Print out the current privileges.
2147- """
2148- if msg is not None:
2149- print(msg)
2150-
2151- print("uid: {}, euid: {}".format(os.getuid(), os.geteuid()))
2152- print("gid: {}, egid: {}".format(os.getgid(), os.getegid()))
2153-
2154-
2155-def write_temp_file(filename=TMP_FILENAME):
2156- """
2157- Write the contents to a temp file.
2158- """
2159- with open(filename, 'w') as fp:
2160- fp.write(TMP_CONTENT)
2161- fp.close()
2162-
2163-
2164-def read_temp_file(filename=TMP_FILENAME):
2165- """
2166- Print the contents of a temp file.
2167- """
2168- with open(filename, 'r') as fp:
2169- for line in fp:
2170- print(line.strip())
2171- fp.close()
2172-
2173-
2174-def _do_cmd(cmd, quiet=False, shell=False):
2175- """
2176- Run a command.
2177- """
2178- cmd_args = cmd
2179-
2180- if not shell:
2181- cmd_args = shlex.split(cmd)
2182-
2183- proc = subprocess.Popen(
2184- cmd_args,
2185- shell=shell,
2186- stdout=subprocess.PIPE,
2187- stderr=subprocess.PIPE
2188- )
2189-
2190- (stdout, _stderr) = proc.communicate()
2191-
2192- if not quiet:
2193- print("{}:\n{}".format(cmd, stdout))
2194-
2195-
2196-def stat_file(filename=TMP_FILENAME):
2197- proc = subprocess.Popen(['ls', '-al', filename], stdout=subprocess.PIPE,
2198- stderr=subprocess.PIPE)
2199-
2200- stdout, _stderr = proc.communicate()
2201- print("{}: {}".format(filename, stdout))
2202-
2203-
2204-def get_privs():
2205- privs = {}
2206-
2207- privs['uid'] = os.getuid()
2208- privs['euid'] = os.geteuid()
2209- privs['gid'] = os.getgid()
2210- privs['egid'] = os.getegid()
2211-
2212- return privs
2213-
2214-
2215-class baseTestPriv(unittest.TestCase):
2216-
2217- def assertRoot(self, msg="Must be root"):
2218- self.assertEqual(os.geteuid(), 0, msg=msg)
2219-
2220- def assertPrivs(self, privs, msg=None):
2221- # Only check privileges passed in
2222- if 'uid' in privs:
2223- self.assertEqual(os.getuid(), privs['uid'], msg)
2224-
2225- if 'euid' in privs:
2226- self.assertEqual(os.geteuid(), privs['euid'], msg)
2227-
2228- if 'gid' in privs:
2229- self.assertEqual(os.getuid(), privs['uid'], msg)
2230-
2231- if 'egid' in privs:
2232- self.assertEqual(os.geteuid(), privs['euid'], msg)
2233-
2234- def setUp(self):
2235- print_priv("before")
2236- _do_cmd("id")
2237-
2238- def tearDown(self):
2239- print_priv("after")
2240- _do_cmd("id")
2241-
2242-
2243-class TestPriv(baseTestPriv):
2244- def test_drop_priv(self):
2245- """
2246- Test that we drop effective privileges appropriately.
2247- """
2248- self.assertRoot()
2249- old_privs = drop_privileges()
2250-
2251- self.assertPrivs({'euid': NOBODY_UID, 'egid': NOGROUP_GID})
2252-
2253- print_priv("middle")
2254- _do_cmd("id")
2255-
2256- write_temp_file()
2257- read_temp_file()
2258- stat_file()
2259-
2260- raise_privileges(old_privs)
2261-
2262- def test_restore_priv(self):
2263- """
2264- Test that we restore effective privileges appropriately.
2265- """
2266- self.assertRoot()
2267- old_privs = drop_privileges()
2268-
2269- self.assertPrivs({'euid': NOBODY_UID, 'egid': NOGROUP_GID})
2270-
2271- print_priv("middle")
2272-
2273- raise_privileges(old_privs)
2274-
2275- self.assertPrivs(old_privs)
2276-
2277-
2278-class TestXXXLast(baseTestPriv):
2279- """
2280- Test's to be run last.
2281- """
2282- def test_xxx_drop_priv_full(self):
2283- """
2284- Test dropping all privileges.
2285- """
2286-
2287- self.assertRoot()
2288- drop_privileges(full=True)
2289- self.assertPrivs({
2290- 'uid': NOBODY_UID,
2291- 'euid': NOBODY_UID,
2292- 'gid': NOGROUP_GID,
2293- 'egid': NOGROUP_GID,
2294- })
2295-
2296- _do_cmd("id")
2297
2298=== modified file 'utah/client/tests/test_common.py'
2299--- utah/client/tests/test_common.py 2013-01-29 14:56:33 +0000
2300+++ utah/client/tests/test_common.py 2013-04-08 19:17:22 +0000
2301@@ -13,11 +13,15 @@
2302 # You should have received a copy of the GNU General Public License along
2303 # with this program. If not, see <http://www.gnu.org/licenses/>.
2304
2305+"""Test utah client common functions."""
2306+
2307 import os
2308+import signal
2309 import unittest
2310
2311 # REQUIRES that the top level utah package be in the Python path.
2312 from utah.client.common import (
2313+ _get_process_children,
2314 get_media_info,
2315 get_product_uuid,
2316 get_host_info,
2317@@ -28,12 +32,11 @@
2318
2319
2320 class TestCommon(unittest.TestCase):
2321- """
2322- Tests for utah.client.common.
2323- """
2324+
2325+ """Tests for utah.client.common."""
2326
2327 def test_get_product_uuid(self):
2328- """ Test that product_uuid is returned when possible. """
2329+ """Test that product_uuid is returned when possible."""
2330
2331 product_uuid = get_product_uuid()
2332
2333@@ -41,9 +44,7 @@
2334 self.assertIsNotNone(product_uuid)
2335
2336 def test_get_media_info(self):
2337- """
2338- Test that if there is a media-info file that it is parsed.
2339- """
2340+ """Test that if there is a media-info file that it is parsed."""
2341 media_info = get_media_info()
2342
2343 print("media_info: {}".format(media_info))
2344@@ -54,10 +55,7 @@
2345 self.assertTrue(media_info == 'unknown')
2346
2347 def test_get_host_info(self):
2348- """
2349- Test that get_host_info returns results.
2350- """
2351-
2352+ """Test that get_host_info returns results."""
2353 host_info = get_host_info()
2354
2355 print("host_info: {}".format(host_info))
2356@@ -65,14 +63,17 @@
2357 self.assertTrue(host_info is not None)
2358
2359 def test_run_cmd(self):
2360+ """Test running a command."""
2361 result = run_cmd('find . -name "*.py"')
2362
2363 self.assertEqual(result['returncode'], 0)
2364
2365 def test_debug_print(self):
2366- """
2367- Test that debug_print doesn't print when DEBUG is False unless
2368- 'debug' is passed in and is True.
2369+ """Test that debug_print prints at the appropriate times.
2370+
2371+ It shouldn't print when DEBUG is False unless 'debug' is passed in and
2372+ is True.
2373+
2374 """
2375 old_debug = CONFIG['DEBUG']
2376
2377@@ -94,9 +95,7 @@
2378 CONFIG['DEBUG'] = old_debug
2379
2380 def test_run_as(self):
2381- """
2382- Test that run_as functions correctly.
2383- """
2384+ """Test that run_as functions correctly."""
2385 user = "nobody"
2386 result = run_cmd("touch /tmp/run_as_test.tmp", run_as=user)
2387 print("result: {}".format(result))
2388@@ -111,10 +110,7 @@
2389 self.assertEqual(result['user'], user)
2390
2391 def test_run_as_default(self):
2392- """
2393- Test that the default user is correctly identified.
2394- """
2395-
2396+ """Test that the default user is correctly identified."""
2397 result = run_cmd("touch /tmp/run_as_test.tmp")
2398 print("result: {}".format(result))
2399
2400@@ -126,3 +122,22 @@
2401
2402 self.assertEqual(result['returncode'], 0)
2403 self.assertNotEqual(result['user'], "unknown")
2404+
2405+ def test_get_process_children(self):
2406+ """Test that the get_process_children function works."""
2407+ children = []
2408+ for _ in xrange(3):
2409+ pid = os.fork()
2410+ if pid == 0:
2411+ # child to wait
2412+ os.execv('/bin/sleep', ['/bin/sleep', '5'])
2413+ children.append(pid)
2414+
2415+ ret = _get_process_children(str(os.getpid()))
2416+ # the last PID from childre will be the "ps" process we ran, so
2417+ # we can ignore that one
2418+ ret.pop()
2419+ for pid in children:
2420+ os.kill(pid, signal.SIGTERM)
2421+ os.waitpid(pid, 0)
2422+ self.assertEqual(children, ret)
2423
2424=== modified file 'utah/client/tests/test_jsonschema.py'
2425--- utah/client/tests/test_jsonschema.py 2012-12-11 09:30:00 +0000
2426+++ utah/client/tests/test_jsonschema.py 2013-04-08 19:17:22 +0000
2427@@ -13,6 +13,9 @@
2428 # You should have received a copy of the GNU General Public License along
2429 # with this program. If not, see <http://www.gnu.org/licenses/>.
2430
2431+"""Test schema parsing functionality."""
2432+
2433+
2434 import jsonschema
2435 import os
2436 import unittest
2437@@ -80,25 +83,22 @@
2438
2439 class TestJSONSchema(unittest.TestCase):
2440
2441+ """Test schema validation."""
2442+
2443 def test_orig_schema(self):
2444- """
2445- Test that the original master.run is correctly validated by the schema.
2446- """
2447-
2448+ """Test that the original master.run validates correctly."""
2449 data = yaml.load(yaml_content)
2450 print("data: {}".format(data))
2451 jsonschema.validate(data, Runner.MASTER_RUNLIST_SCHEMA)
2452
2453 def test_multi_schema(self):
2454- """
2455- Test that the new master.run is correctly validated by the schema.
2456- """
2457-
2458+ """Test that the new master.run validates correctly."""
2459 data_new = yaml.load(yaml_content_new)
2460 print("data_new: {}".format(data_new))
2461 jsonschema.validate(data_new, Runner.MASTER_RUNLIST_SCHEMA)
2462
2463 def test_example_runlist(self):
2464+ """Test that the example runlist validates correctly."""
2465 example_runlist = os.path.join(os.path.dirname(__file__),
2466 "../examples/master.run")
2467
2468@@ -111,19 +111,13 @@
2469 fp.close()
2470
2471 def test_include_schema(self):
2472- """
2473- Test that include works correctly.
2474- """
2475-
2476+ """Test that include works correctly."""
2477 data = yaml.load(yaml_content_include)
2478 print("data: {}".format(data))
2479 jsonschema.validate(data, Runner.MASTER_RUNLIST_SCHEMA)
2480
2481 def test_bad_schemas(self):
2482- """
2483- Test that required fields are enforced by the schema.
2484- """
2485-
2486+ """Test that required fields are enforced by the schema."""
2487 for content in yaml_content_bad:
2488 data = yaml.load(content)
2489 print("data: {}".format(data))
2490
2491=== modified file 'utah/client/tests/test_misc.py'
2492--- utah/client/tests/test_misc.py 2012-12-03 14:02:18 +0000
2493+++ utah/client/tests/test_misc.py 2013-04-08 19:17:22 +0000
2494@@ -13,15 +13,21 @@
2495 # You should have received a copy of the GNU General Public License along
2496 # with this program. If not, see <http://www.gnu.org/licenses/>.
2497
2498+"""Test miscellaneous functionality."""
2499+
2500+
2501 import os
2502 import unittest
2503
2504-from .common import get_module_path
2505+from utah.client.tests.common import get_module_path
2506
2507
2508 class TestMisc(unittest.TestCase):
2509+
2510+ """Test miscellaneous functionality."""
2511+
2512 def test_get_module_path(self):
2513-
2514+ """Test getting the module path."""
2515 print("__file__: {}".format(__file__))
2516
2517 # assumes this file is in 'utah/client/tests'
2518
2519=== modified file 'utah/client/tests/test_phoenix.py'
2520--- utah/client/tests/test_phoenix.py 2013-03-21 15:51:53 +0000
2521+++ utah/client/tests/test_phoenix.py 2013-04-08 19:17:22 +0000
2522@@ -13,6 +13,9 @@
2523 # You should have received a copy of the GNU General Public License along
2524 # with this program. If not, see <http://www.gnu.org/licenses/>.
2525
2526+"""Test phoenix metadata generation tool."""
2527+
2528+
2529 import os
2530 import shutil
2531 import unittest
2532@@ -23,21 +26,27 @@
2533
2534
2535 def setUp():
2536+ """Set up test prerequisite."""
2537 if os.path.exists(directory_name):
2538 raise Exception("directory '{}' already exists".format(directory_name))
2539
2540
2541 def tearDown():
2542+ """Remove test resources."""
2543 if os.path.exists(directory_name):
2544 shutil.rmtree(directory_name)
2545
2546
2547 class TestPhoenix(unittest.TestCase):
2548+
2549+ """Test phoenix test suite creation utility."""
2550+
2551 testsuite_name = "testsuite_name"
2552 testcases = ['test_one', 'test_two']
2553 directory_name = directory_name
2554
2555 def setUp(self):
2556+ """Set up test suite creation information."""
2557 self.phoenix = Phoenix(
2558 self.testsuite_name,
2559 directory=self.directory_name,
2560@@ -45,61 +54,43 @@
2561 )
2562
2563 def test_missing_required_args(self):
2564- """
2565- Test that __init__ fails without the required arguments
2566- """
2567+ """Test that __init__ fails without the required arguments."""
2568 self.assertRaises(
2569 TypeError,
2570 Phoenix,
2571 )
2572
2573 def test_providing_required_args(self):
2574- """
2575- Test that __init__ succeeds with the required arguments.
2576- """
2577-
2578+ """Test that __init__ succeeds with the required arguments."""
2579 self.assertTrue(self.phoenix is not None)
2580
2581 def test_suite_name(self):
2582- """
2583- Test that the testsuite name is correctly set.
2584- """
2585-
2586+ """Test that the testsuite name is correctly set."""
2587 self.assertEqual(self.phoenix.testsuite, self.testsuite_name)
2588
2589 def test_directory_name(self):
2590- """
2591- Test that the directory name is correctly set.
2592- """
2593-
2594+ """Test that the directory name is correctly set."""
2595 self.assertEqual(self.phoenix.directory, self.directory_name)
2596
2597 def test_default_directory_name(self):
2598- """
2599- Test that the default directory name is correctly set.
2600- """
2601-
2602+ """Test that the default directory name is correctly set."""
2603 phoenix = Phoenix(self.testsuite_name)
2604
2605 self.assertEqual(phoenix.directory, '.')
2606
2607 def test_testcases(self):
2608- """
2609- Test that the testcases are correctly set.
2610- """
2611-
2612+ """Test that the testcases are correctly set."""
2613 self.assertListEqual(self.phoenix.testcases, self.testcases)
2614
2615 def test_testcases_default(self):
2616- """
2617- Test that the default testcase is correctly set.
2618- """
2619+ """Test that the default testcase is correctly set."""
2620
2621 phoenix = Phoenix(self.testsuite_name)
2622 # there should be only one default testcase.
2623 self.assertEqual(len(phoenix.testcases), 1)
2624
2625 def test_build_suite(self):
2626+ """Test building a test suite."""
2627 self.phoenix.build_suite()
2628
2629 dir_path = self.directory_name
2630@@ -118,9 +109,7 @@
2631 "{} doesn't exist".format(path))
2632
2633 def test_create_file(self):
2634- """
2635- Test that Phoenix create file works correctly.
2636- """
2637+ """Test that Phoenix create file works correctly."""
2638 fn = '/tmp/phoenix_create_file_test.file'
2639 fc = 'a test file'
2640
2641
2642=== modified file 'utah/client/tests/test_result.py'
2643--- utah/client/tests/test_result.py 2013-03-20 19:20:13 +0000
2644+++ utah/client/tests/test_result.py 2013-04-08 19:17:22 +0000
2645@@ -13,6 +13,9 @@
2646 # You should have received a copy of the GNU General Public License along
2647 # with this program. If not, see <http://www.gnu.org/licenses/>.
2648
2649+"""Test our ability to report results."""
2650+
2651+
2652 import unittest
2653 import yaml
2654
2655@@ -23,7 +26,11 @@
2656
2657
2658 class TestResult(unittest.TestCase):
2659+
2660+ """Test result processing and display functionality."""
2661+
2662 def setUp(self):
2663+ """Create needed data structures."""
2664 self.result_yaml = ResultYAML("result_yaml")
2665 self.result_text = Result("result")
2666
2667@@ -44,58 +51,49 @@
2668 }
2669
2670 def test_add_result(self):
2671+ """Test adding test results."""
2672 self.result_yaml.add_result(self.result)
2673
2674- self.assertEqual(self.result_yaml.count_results(), 1)
2675+ self.assertEqual(self.result_yaml._count_results(), 1)
2676
2677 self.result_yaml.result()
2678
2679 def test_result(self):
2680+ """Test generating results."""
2681 self.result_yaml.add_result(self.result)
2682
2683 # check that the result was added.
2684- self.assertEqual(self.result_yaml.count_results(), 1)
2685+ self.assertEqual(self.result_yaml._count_results(), 1)
2686
2687 self.result_yaml.result()
2688
2689 # check that the result was processed and removed.
2690- self.assertEqual(self.result_yaml.count_results(), 0)
2691+ self.assertEqual(self.result_yaml._count_results(), 0)
2692
2693 def test_result_pass(self):
2694- """
2695- Test that adding a pass result sets the Result object's status
2696- to 'PASS'.
2697- """
2698+ """Test that adding a pass result sets the status to 'PASS'."""
2699 self.result_yaml.add_result(self.result)
2700
2701 self.assertEqual(self.result_yaml.status, 'PASS')
2702
2703 def test_result_failure(self):
2704- """
2705- Test that adding a failure result sets the Result object's status
2706- to 'ERROR'.
2707- """
2708+ """Test that adding a failure result sets the status to 'FAIL'."""
2709 self.result_yaml.add_result(self.result_fail)
2710
2711 self.assertEqual(self.result_yaml.status, 'FAIL')
2712
2713 def test_result_clear_result(self):
2714- """
2715- Test that the result is actually cleared.
2716- """
2717+ """Test that the result is actually cleared."""
2718 self.result_yaml.add_result(self.result_fail)
2719 self.assertEqual(self.result_yaml.status, 'FAIL')
2720
2721- self.result_yaml.clear_results()
2722+ self.result_yaml._clear_results()
2723
2724 self.assertEqual(self.result_yaml.status, 'PASS')
2725- self.assertEqual(self.result_yaml.count_results(), 0)
2726+ self.assertEqual(self.result_yaml._count_results(), 0)
2727
2728 def test_result_result(self):
2729- """
2730- Test that ERROR status is retained after adding pass
2731- results.
2732- """
2733+ """Test that ERROR status is retained after adding pass results."""
2734 self.result_yaml.add_result(self.result_fail)
2735
2736 self.assertEqual(self.result_yaml.status, 'FAIL')
2737@@ -105,6 +103,7 @@
2738 self.assertEqual(self.result_yaml.status, 'FAIL')
2739
2740 def test_result_yaml(self):
2741+ """Test generating YAML results."""
2742 self.result_yaml.add_result(self.result)
2743
2744 import sys
2745@@ -122,7 +121,7 @@
2746
2747 result_str = str_output.getvalue()
2748
2749- print("result_str: %s" % result_str)
2750+ print('result_str: {}'.format(result_str))
2751
2752 data = yaml.load(result_str)
2753
2754@@ -130,7 +129,7 @@
2755
2756 result = data['commands'][0]
2757
2758- print("data: %s" % data)
2759+ print('data: {}'.format(data))
2760
2761 self.assertEqual(result['command'], self.result['command'])
2762 self.assertEqual(result['returncode'], self.result['returncode'])
2763
2764=== modified file 'utah/client/tests/test_runner.py'
2765--- utah/client/tests/test_runner.py 2013-03-21 21:04:14 +0000
2766+++ utah/client/tests/test_runner.py 2013-04-08 19:17:22 +0000
2767@@ -13,6 +13,9 @@
2768 # You should have received a copy of the GNU General Public License along
2769 # with this program. If not, see <http://www.gnu.org/licenses/>.
2770
2771+"""Who tests the test runners?"""
2772+
2773+
2774 import os
2775 import shutil
2776 import unittest
2777@@ -24,7 +27,7 @@
2778 from utah.client.common import ReturnCodes
2779 from utah.client import exceptions
2780
2781-from .common import ( # NOQA
2782+from utah.client.tests.common import ( # NOQA
2783 setUp, # Used by nosetests
2784 tearDown, # Used by nosetests
2785 master_runlist_content,
2786@@ -33,7 +36,11 @@
2787
2788
2789 class TestRunner(unittest.TestCase):
2790+
2791+ """Test the TestRunner class."""
2792+
2793 def setUp(self):
2794+ """Set up test prerequisites."""
2795 self.result_class = ResultYAML
2796 self.state_file = '/tmp/state.yaml'
2797 self.state_agent = StateAgentYAML(state_file=self.state_file)
2798@@ -46,7 +53,7 @@
2799 "{} shouldn't exist".format(self.bad_dir))
2800
2801 def tearDown(self):
2802-
2803+ """Clean up test resources."""
2804 # Remove self.bad_dir if it got created.
2805 try:
2806 shutil.rmtree(self.bad_dir)
2807@@ -58,6 +65,7 @@
2808 raise
2809
2810 def test_fetch_failed(self):
2811+ """Test handling of failed fetch commands."""
2812 bad_runlist_content = """---
2813 testsuites:
2814 - name: fake_tests
2815@@ -75,17 +83,18 @@
2816 os.remove(runlist)
2817
2818 runner.run()
2819- print runner.report()
2820
2821 self.assertEqual(runner.fetch_errors, 1)
2822
2823 def test_rc_local(self):
2824+ """Test rc.local functionality for reboots."""
2825 tmp_rc_local = '/tmp/rc.local'
2826 self.runner.setup_rc_local(tmp_rc_local)
2827
2828 self.assertTrue(os.path.exists(tmp_rc_local))
2829
2830 def test_run(self):
2831+ """Test running all test suites."""
2832 retcode = self.runner.run()
2833
2834 for suite in self.runner.suites:
2835@@ -94,9 +103,7 @@
2836 self.assertEqual(retcode, ReturnCodes.FAIL)
2837
2838 def test_missing_testdir(self):
2839- """
2840- Test that the runner fails gracefully on missing testdir.
2841- """
2842+ """Test that the runner fails gracefully on missing testdir."""
2843 self.assertRaises(exceptions.BadDir,
2844 Runner,
2845 install_type="desktop",
2846@@ -104,10 +111,7 @@
2847 testdir=self.bad_dir)
2848
2849 def test_missing_master_runlist(self):
2850- """
2851- Test that the runner fails gracefully on missing 'master.run'
2852- """
2853-
2854+ """Test that the runner fails gracefully on missing 'master.run'."""
2855 tmp_master_runlist = os.path.join(self.bad_dir, 'master.run')
2856
2857 if not os.path.exists(self.bad_dir):
2858@@ -125,9 +129,7 @@
2859 testdir=self.bad_dir)
2860
2861 def test_runlist(self):
2862- """
2863- Test that passing a runlist to the Runner works properly.
2864- """
2865+ """Test that passing a runlist to the Runner works properly."""
2866 runlist = '/tmp/master_runlist_test'
2867
2868 with open(runlist, 'w') as fp:
2869@@ -145,17 +147,14 @@
2870 os.remove(runlist)
2871
2872 def test_testdir(self):
2873- """
2874- Test that the default test directory is created.
2875- """
2876-
2877+ """Test that the default test directory is created."""
2878 testdir = self.runner.testdir
2879
2880 self.assertTrue(os.path.exists(testdir))
2881
2882 @patch('utah.client.runner.TestSuite')
2883 def test_include_exclude(self, mock_class):
2884- """Test that includes/excludes are passed to the test suite"""
2885+ """Test that includes/excludes are passed to the test suite."""
2886 include_runlist_content = """---
2887 testsuites:
2888 - name: utah_tests
2889
2890=== modified file 'utah/client/tests/test_state_agent.py'
2891--- utah/client/tests/test_state_agent.py 2013-03-21 21:22:10 +0000
2892+++ utah/client/tests/test_state_agent.py 2013-04-08 19:17:22 +0000
2893@@ -13,6 +13,9 @@
2894 # You should have received a copy of the GNU General Public License along
2895 # with this program. If not, see <http://www.gnu.org/licenses/>.
2896
2897+"""Test our ability to save and restore state information."""
2898+
2899+
2900 import os
2901 import unittest
2902 import yaml
2903@@ -21,7 +24,7 @@
2904 from utah.client.runner import Runner
2905 from utah.client.result import ResultYAML
2906
2907-from utah.client.tests.common import (
2908+from utah.client.tests.common import ( # NOQA
2909 setUp, # Used by nosetests
2910 tearDown, # Used by nosetests
2911 partial_state_file_content,
2912@@ -31,10 +34,11 @@
2913
2914
2915 class TestStateAgentYAML(unittest.TestCase):
2916+
2917+ """Test the YAML state agent."""
2918+
2919 def setUp(self):
2920- """
2921- Create a state agent for testing.
2922- """
2923+ """Create a state agent for testing."""
2924 self.state_file = '/tmp/state.yaml'
2925
2926 if os.path.exists(self.state_file):
2927@@ -46,19 +50,18 @@
2928 state_agent=self.state_agent)
2929
2930 def tearDown(self):
2931- """
2932- Clean up the test state file.
2933- """
2934-
2935+ """Clean up the test state file."""
2936 if os.path.exists(self.state_file):
2937 os.remove(self.state_file)
2938
2939 def test_creation(self):
2940+ """Test that state file is correctly created."""
2941 self.runner.run()
2942
2943 self.assertTrue(os.path.exists(self.state_file))
2944
2945 def test_cleanup(self):
2946+ """Test that cleanup removes state file."""
2947 self.runner.save_state()
2948
2949 self.assertTrue(os.path.exists(self.state_file),
2950@@ -67,12 +70,11 @@
2951 self.state_agent.clean()
2952
2953 self.assertFalse(os.path.exists(self.state_file),
2954- "Failed to remove state file (%s)" % self.state_file)
2955+ 'Failed to remove state file ({})'
2956+ .format(self.state_file))
2957
2958 def test_status(self):
2959- """
2960- Test that a runner properly saves its state during test runs.
2961- """
2962+ """Test that a runner properly saves its state during test runs."""
2963 suite_count = self.runner.count_suites()
2964 test_count = self.runner.count_tests()
2965
2966@@ -121,21 +123,15 @@
2967 self.assertEqual(runner.passes + runner.failures + runner.errors, t)
2968
2969 def test_load_state_partial(self):
2970- """
2971- Test that a partially run state file can be resumed.
2972- """
2973+ """Test that a partially run state file can be resumed."""
2974 self._test_partial(partial_state_file_content, 'test_two', 4, 1, 0)
2975
2976 def test_load_state_partial_run_all(self):
2977- """
2978- Test partially run state file - run all jobs.
2979- """
2980+ """Test partially run state file - run all jobs."""
2981 self._test_partial(
2982 partial_state_file_content_run_all, 'test_one', 3, 2, 0)
2983
2984 def test_load_state_partial_done_all_failed(self):
2985- """
2986- Test partially run state file - all jobs failed.
2987- """
2988+ """Test partially run state file - all jobs failed."""
2989 self._test_partial(
2990 partial_state_file_content_done_all_failed, None, 0, 5, 0)
2991
2992=== modified file 'utah/client/tests/test_testcase.py'
2993--- utah/client/tests/test_testcase.py 2013-02-11 19:41:45 +0000
2994+++ utah/client/tests/test_testcase.py 2013-04-08 19:17:22 +0000
2995@@ -13,27 +13,30 @@
2996 # You should have received a copy of the GNU General Public License along
2997 # with this program. If not, see <http://www.gnu.org/licenses/>.
2998
2999+"""Test handling of test cases."""
3000+
3001+
3002 import os
3003 import unittest
3004
3005+from utah.client import testcase
3006+from utah.client.common import (
3007+ UTAH_DIR,
3008+)
3009 from utah.client.exceptions import MissingData
3010 from utah.client.result import ResultYAML
3011-from utah.client.common import (
3012- UTAH_DIR,
3013-)
3014-from utah.client import testcase
3015-
3016-from .common import ( # NOQA
3017+from utah.client.tests.common import ( # NOQA
3018 setUp, # Used by nosetests
3019 tearDown, # Used by nosetests
3020 )
3021
3022
3023 class TestTestCase(unittest.TestCase):
3024- """
3025- Tests for the TestCase class.
3026- """
3027+
3028+ """Tests for the TestCase class."""
3029+
3030 def setUp(self):
3031+ """Initialize basic test case data."""
3032 self.timeout = 10
3033 self.command = 'echo "command"'
3034 self.result = ResultYAML()
3035@@ -53,9 +56,7 @@
3036 )
3037
3038 def test_run(self):
3039- """
3040- Test that a TestCase can be run.
3041- """
3042+ """Test that a TestCase can be run."""
3043 self.case.run()
3044
3045 # make sure after a testcase run that the cwd hasn't changed.
3046@@ -63,12 +64,15 @@
3047 self.assertTrue(self.case.is_done())
3048
3049 def test_timeout(self):
3050+ """Test that timeouts are parsed correctly."""
3051 self.assertEqual(self.case.timeout, self.timeout)
3052
3053 def test_command(self):
3054+ """Test that commands are parsed correctly."""
3055 self.assertEqual(self.case.command, self.command)
3056
3057 def test_save_state(self):
3058+ """Test that state can be saved."""
3059 state = self.case.save_state()
3060
3061 self.assertEqual(state['status'], 'NOTRUN')
3062@@ -76,6 +80,7 @@
3063 self.assertEqual(state['timeout'], self.timeout)
3064
3065 def test_save_and_load_state(self):
3066+ """Test that state can be saved and loaded."""
3067 state = self.case.save_state()
3068
3069 case = testcase.TestCase(
3070@@ -97,6 +102,7 @@
3071 self.assertTrue(case.is_done())
3072
3073 def test_timeout_override(self):
3074+ """Test that overriding timeouts works."""
3075 timeout = 99
3076 case = testcase.TestCase(
3077 name=self.name,
3078@@ -108,10 +114,7 @@
3079 self.assertEqual(timeout, case.timeout)
3080
3081 def test_control_data(self):
3082- """
3083- Test that a correctly formed control_data that's passed in
3084- doesn't raise an exception.
3085- """
3086+ """Test that valid control_data doesn't raise an exception."""
3087 control_data = {
3088 'description': 'a test case',
3089 'command': '/bin/true',
3090@@ -137,10 +140,7 @@
3091 self.assertTrue(case.is_done())
3092
3093 def test_bad_control_data(self):
3094- """
3095- Test that an incorrectly formed control_data that's passed in
3096- does raise an exception.
3097- """
3098+ """Test that bad control_data does raise an exception."""
3099 control_data = {
3100 'description': 'a test case',
3101 'command': '/bin/true',
3102
3103=== modified file 'utah/client/tests/test_testsuite.py'
3104--- utah/client/tests/test_testsuite.py 2012-12-11 09:47:51 +0000
3105+++ utah/client/tests/test_testsuite.py 2013-04-08 19:17:22 +0000
3106@@ -13,6 +13,9 @@
3107 # You should have received a copy of the GNU General Public License along
3108 # with this program. If not, see <http://www.gnu.org/licenses/>.
3109
3110+"""Test handling of test suites."""
3111+
3112+
3113 import os
3114 import unittest
3115
3116@@ -22,24 +25,23 @@
3117 from utah.client.result import ResultYAML
3118 from utah.client import testsuite
3119
3120-from .common import ( # NOQA
3121+from utah.client.tests.common import ( # NOQA
3122 setUp, # Used by nosetests
3123 tearDown, # Used by nosetests
3124 )
3125
3126
3127 def state_saver():
3128+ """Provide state saving functionality for testing."""
3129 debug_print("Saving state", force=True)
3130
3131
3132 class TestTestSuite(unittest.TestCase):
3133- """
3134- Tests for the TestSuite class.
3135- """
3136+
3137+ """Tests for the TestSuite class."""
3138+
3139 def setUp(self):
3140- """
3141- Use the 'examples' test suite that's part of the UTAH package.
3142- """
3143+ """Use the 'examples' test suite that's part of the UTAH package."""
3144 self.name = 'examples'
3145 self.path = '/var/lib/utah/testsuites'
3146 self.assertTrue(os.path.exists(os.path.join(self.path, self.name)))
3147@@ -49,23 +51,28 @@
3148 _save_state_callback=state_saver)
3149
3150 def test_run(self):
3151- print "found %d tests" % self.suite.count_tests()
3152+ """Test running a test suite."""
3153+ print 'found {} tests'.format(str(self.suite.count_tests()))
3154 self.suite.run()
3155 self.assertEqual(self.suite.result.status, 'PASS')
3156
3157 def test_count_tests(self):
3158+ """Test ability of test suite to count tests."""
3159 self.assertEquals(self.suite.count_tests(), 2)
3160
3161 def test_next_test(self):
3162+ """Test ability of test suite to indicate next test to run."""
3163 next_test = self.suite.get_next_test()
3164
3165 self.assertTrue(hasattr(next_test, 'name'))
3166 self.assertEquals(self.suite.get_next_test().name, 'test_one')
3167
3168 def test_is_done(self):
3169+ """Test test suite completion reporting."""
3170 self.assertFalse(self.suite.is_done())
3171
3172 def test_save_state(self):
3173+ """Test saving test suite state."""
3174 state = self.suite.save_state()
3175
3176 print state
3177@@ -79,6 +86,7 @@
3178 self.assertEqual(tests, self.suite.count_tests())
3179
3180 def test_timeout_override(self):
3181+ """Test overriding a timeout value."""
3182 timeout = 101
3183 suite = testsuite.TestSuite(
3184 name=self.name,
3185@@ -94,7 +102,7 @@
3186 self.assertEqual(timeout, test.timeout)
3187
3188 def test_include(self):
3189- """Included tests are executed"""
3190+ """Verify that included tests are executed."""
3191 included_test_name = 'test_one'
3192
3193 suite = testsuite.TestSuite(
3194@@ -112,7 +120,7 @@
3195 self.assertEqual(case.name, included_test_name)
3196
3197 def test_exclude(self):
3198- """Excluded tests are *not* executed"""
3199+ """Verify that excluded tests are not executed."""
3200 excluded_test_name = 'test_two'
3201
3202 suite = testsuite.TestSuite(
3203@@ -130,7 +138,7 @@
3204 self.assertNotEqual(case.name, excluded_test_name)
3205
3206 def test_include_exclude(self):
3207- """Include/exclude combination works fine"""
3208+ """Verify that include/exclude combination works fine."""
3209 included_test_name = 'test_one'
3210 excluded_test_name = 'test_two'
3211
3212
3213=== modified file 'utah/client/tests/test_vcs_bzr.py'
3214--- utah/client/tests/test_vcs_bzr.py 2012-12-12 11:22:58 +0000
3215+++ utah/client/tests/test_vcs_bzr.py 2013-04-08 19:17:22 +0000
3216@@ -13,20 +13,22 @@
3217 # You should have received a copy of the GNU General Public License along
3218 # with this program. If not, see <http://www.gnu.org/licenses/>.
3219
3220+"""Test bzr interface."""
3221+
3222+
3223 import os
3224 import shutil
3225 import unittest
3226
3227-from utah.client.common import BzrHandler, run_cmd
3228+from utah.client.common import run_cmd
3229+from utah.client.vcs import BzrHandler
3230+
3231
3232 BZR_TEST_REPO = "/tmp/utahbzrtest"
3233
3234
3235 def setUp():
3236- """
3237- Create a local bzr repository to test with.
3238- """
3239-
3240+ """Create a local bzr repository for testing."""
3241 # should fail if the repo already exists.
3242 os.mkdir(BZR_TEST_REPO)
3243
3244@@ -37,24 +39,17 @@
3245
3246
3247 def tearDown():
3248- """
3249- Remove the test repo.
3250- """
3251-
3252+ """Remove the test repo."""
3253 # We should only ever get here if the repo was created in setUp().
3254 shutil.rmtree(BZR_TEST_REPO)
3255
3256
3257 class TestBzrHandler(unittest.TestCase):
3258- """
3259- Test the bazaar VCS handler.
3260- """
3261+
3262+ """Test the bazaar VCS handler."""
3263
3264 def test_init(self):
3265- """
3266- Test the initial setup.
3267- """
3268-
3269+ """Test the initial setup."""
3270 repo = BZR_TEST_REPO
3271 bzr = BzrHandler(repo=repo)
3272
3273@@ -64,6 +59,7 @@
3274 self.assertEqual(bzr.rev_command, "bzr revno")
3275
3276 def test_bzr_get(self):
3277+ """Test retrieving a bzr repo."""
3278 repo = BZR_TEST_REPO
3279 dest = "utah-dev"
3280 bzr = BzrHandler(repo=repo, destination=dest)
3281@@ -79,6 +75,7 @@
3282 shutil.rmtree(path)
3283
3284 def test_bzr_revision(self):
3285+ """Test getting revision information from a bzr repo."""
3286 repo = BZR_TEST_REPO
3287 dest = "utah-dev"
3288 bzr = BzrHandler(repo=repo, destination=dest)
3289@@ -92,7 +89,7 @@
3290
3291 self.assertTrue(os.path.exists(path), path)
3292
3293- result = bzr.get_revision(directory=path)
3294+ result = bzr.revision(directory=path)
3295
3296 shutil.rmtree(path)
3297- self.assertIsInstance(int(result), int)
3298+ self.assertEqual(result['returncode'], 0)
3299
3300=== modified file 'utah/client/tests/test_vcs_dev.py'
3301--- utah/client/tests/test_vcs_dev.py 2012-12-12 11:22:58 +0000
3302+++ utah/client/tests/test_vcs_dev.py 2013-04-08 19:17:22 +0000
3303@@ -13,20 +13,22 @@
3304 # You should have received a copy of the GNU General Public License along
3305 # with this program. If not, see <http://www.gnu.org/licenses/>.
3306
3307+"""Test the dev pseudo-VCS interface."""
3308+
3309+
3310 import os
3311 import shutil
3312 import unittest
3313
3314-from utah.client.common import DevHandler, run_cmd
3315+from utah.client.common import run_cmd
3316+from utah.client.vcs import DevHandler
3317+
3318
3319 DEV_TEST_REPO = "/tmp/utahdevtest"
3320
3321
3322 def setUp():
3323- """
3324- Create a local directory to test on.
3325- """
3326-
3327+ """Create a local directory for testing."""
3328 # should fail if the repo already exists.
3329 os.mkdir(DEV_TEST_REPO)
3330
3331@@ -34,24 +36,17 @@
3332
3333
3334 def tearDown():
3335- """
3336- Remove the test repo.
3337- """
3338-
3339+ """Remove the test repo."""
3340 # We should only ever get here if the repo was created in setUp().
3341 shutil.rmtree(DEV_TEST_REPO)
3342
3343
3344 class TestDevHandler(unittest.TestCase):
3345- """
3346- Test the bazaar VCS handler.
3347- """
3348+
3349+ """Test the dev pseudo-VCS handler."""
3350
3351 def test_init(self):
3352- """
3353- Test the initial setup.
3354- """
3355-
3356+ """Test the initial setup."""
3357 repo = DEV_TEST_REPO
3358 dev = DevHandler(repo=repo)
3359
3360@@ -61,6 +56,7 @@
3361 self.assertEqual(dev.rev_command, "echo 'DEVELOPMENT'")
3362
3363 def test_dev_get(self):
3364+ """Test copying files."""
3365 repo = DEV_TEST_REPO
3366 dest = "utah-dev"
3367 dev = DevHandler(repo=repo, destination=dest)
3368@@ -76,6 +72,7 @@
3369 shutil.rmtree(path)
3370
3371 def test_dev_revision(self):
3372+ """Test fake revision gathering support."""
3373 repo = DEV_TEST_REPO
3374 dest = "utah-dev"
3375 dev = DevHandler(repo=repo, destination=dest)
3376@@ -87,7 +84,7 @@
3377 self.assertEqual(result['returncode'], 0)
3378 self.assertTrue(os.path.exists(path))
3379
3380- result = dev.get_revision(directory=path)
3381+ result = dev.revision(directory=path)
3382
3383 shutil.rmtree(path)
3384- self.assertIsInstance(result, str)
3385+ self.assertEqual(result['returncode'], 0)
3386
3387=== modified file 'utah/client/tests/test_vcs_git.py'
3388--- utah/client/tests/test_vcs_git.py 2012-12-12 11:22:58 +0000
3389+++ utah/client/tests/test_vcs_git.py 2013-04-08 19:17:22 +0000
3390@@ -13,20 +13,22 @@
3391 # You should have received a copy of the GNU General Public License along
3392 # with this program. If not, see <http://www.gnu.org/licenses/>.
3393
3394+"""Test the git interface."""
3395+
3396+
3397 import os
3398 import shutil
3399 import unittest
3400
3401-from utah.client.common import GitHandler, run_cmd
3402+from utah.client.common import run_cmd
3403+from utah.client.vcs import GitHandler
3404+
3405
3406 GIT_TEST_REPO = "/tmp/utahgittest"
3407
3408
3409 def setUp():
3410- """
3411- Create a local git repository to test on.
3412- """
3413-
3414+ """Create a local git repository for testing."""
3415 # should fail if the repo already exists.
3416 os.mkdir(GIT_TEST_REPO)
3417
3418@@ -37,24 +39,17 @@
3419
3420
3421 def tearDown():
3422- """
3423- Remove the test repo.
3424- """
3425-
3426+ """Remove the test repo."""
3427 # We should only ever get here if the repo was created in setUp().
3428 shutil.rmtree(GIT_TEST_REPO)
3429
3430
3431 class TestGitHandler(unittest.TestCase):
3432- """
3433- Test the bazaar VCS handler.
3434- """
3435+
3436+ """Test the bazaar VCS handler."""
3437
3438 def test_init(self):
3439- """
3440- Test the initial setup.
3441- """
3442-
3443+ """Test the initial setup."""
3444 repo = GIT_TEST_REPO
3445 git = GitHandler(repo=repo)
3446
3447@@ -64,6 +59,7 @@
3448 self.assertEqual(git.rev_command, "git rev-parse HEAD")
3449
3450 def test_git_get(self):
3451+ """Test retrieving a git repo."""
3452 repo = GIT_TEST_REPO
3453 dest = "utah-dev"
3454 git = GitHandler(repo=repo, destination=dest)
3455@@ -79,6 +75,7 @@
3456 shutil.rmtree(path)
3457
3458 def test_git_revision(self):
3459+ """Test getting revision information from a git repo."""
3460 repo = GIT_TEST_REPO
3461 dest = "utah-dev"
3462 git = GitHandler(repo=repo, destination=dest)
3463@@ -90,7 +87,7 @@
3464 self.assertEqual(result['returncode'], 0)
3465 self.assertTrue(os.path.exists(path))
3466
3467- result = git.get_revision(directory=path)
3468+ result = git.revision(directory=path)
3469
3470 shutil.rmtree(path)
3471- self.assertIsInstance(result, str)
3472+ self.assertEqual(result['returncode'], 0)
3473
3474=== modified file 'utah/client/tests/test_yaml.py'
3475--- utah/client/tests/test_yaml.py 2012-12-08 02:10:12 +0000
3476+++ utah/client/tests/test_yaml.py 2013-04-08 19:17:22 +0000
3477@@ -13,12 +13,19 @@
3478 # You should have received a copy of the GNU General Public License along
3479 # with this program. If not, see <http://www.gnu.org/licenses/>.
3480
3481+"""Test YAML functionality."""
3482+
3483+
3484 import unittest
3485 import yaml
3486
3487
3488 class TestYAML(unittest.TestCase):
3489+
3490+ """Test YAML processing."""
3491+
3492 def test_parts(self):
3493+ """Test initial YAML processing."""
3494 content = {}
3495 content['metadata'] = {
3496 'runlist': '/tmp/master.run',
3497@@ -41,6 +48,7 @@
3498 print(dec_data)
3499
3500 def test_parts_2(self):
3501+ """Test additional YAML processing."""
3502 content = {
3503 'file': '/tmp/file'
3504 }
3505@@ -76,6 +84,7 @@
3506 print(dec_data)
3507
3508 def test_parts_3(self):
3509+ """Test final YAML processing."""
3510 extra_args = {}
3511 content = [
3512 {
3513
3514=== modified file 'utah/client/testsuite.py'
3515--- utah/client/testsuite.py 2013-03-14 20:36:41 +0000
3516+++ utah/client/testsuite.py 2013-04-08 19:17:22 +0000
3517@@ -13,9 +13,8 @@
3518 # You should have received a copy of the GNU General Public License along
3519 # with this program. If not, see <http://www.gnu.org/licenses/>.
3520
3521-"""
3522-Testsuite specific code.
3523-"""
3524+"""Testsuite specific code."""
3525+
3526
3527 import jsonschema
3528 import os
3529@@ -36,8 +35,11 @@
3530
3531
3532 def parse_runlist_file(runlist_file):
3533- """
3534- Parse a tslist.run runlist file and check agains schema
3535+ """Parse a tslist.run runlist file and check against schema.
3536+
3537+ :returns: Parsed data from the runlist
3538+ :rtype: dict
3539+
3540 """
3541 with open(runlist_file, 'r') as fp:
3542 data = yaml.load(fp)
3543@@ -48,9 +50,8 @@
3544
3545
3546 class TestSuite(object):
3547- """
3548- The TestSuite class.
3549- """
3550+
3551+ """Base class describing a test suite."""
3552
3553 build_cmd = None
3554 timeout = None
3555@@ -138,7 +139,7 @@
3556 control_data = None
3557 except jsonschema.ValidationError as exception:
3558 raise exceptions.ValidationError(
3559- '{!r} test suite control file invalid: {!r}\n1'
3560+ '{!r} test suite control file invalid: {!r}\n'
3561 'Detailed information: {}'
3562 .format(self.name, self.control_file, exception))
3563
3564@@ -192,19 +193,15 @@
3565 self.tests.append(tc)
3566
3567 def __str__(self):
3568- return "%s: %s" % (self.control_file, self.runlist_file)
3569+ return "{}: {}".format(self.control_file, self.runlist_file)
3570
3571 def set_status(self, status):
3572- """
3573- Set the status for the test suite and call the save state callback.
3574- """
3575+ """Set the status and call the save state callback."""
3576 self.status = status
3577 self.save_state_callback()
3578
3579 def build(self, result):
3580- """
3581- Run build, but only if we haven't started a run yet.
3582- """
3583+ """Run build, but only if we haven't started a run yet."""
3584 if self.status == 'NOTRUN':
3585 self.set_status('BUILD')
3586 cmd_result = run_cmd(
3587@@ -215,9 +212,7 @@
3588 result.add_result(cmd_result)
3589
3590 def setup(self, result):
3591- """
3592- Run ts_setup, but only if build() has just passed.
3593- """
3594+ """Run ts_setup, but only if build() has just passed."""
3595 if self.status == 'BUILD' and result.status == 'PASS':
3596 self.set_status('SETUP')
3597 cmd_result = run_cmd(
3598@@ -228,9 +223,7 @@
3599 result.add_result(cmd_result)
3600
3601 def cleanup(self, result):
3602- """
3603- Run ts_cleanup after a run.
3604- """
3605+ """Run ts_cleanup after a run."""
3606 if self.status == 'INPROGRESS':
3607 self.set_status('CLEANUP')
3608 cmd_result = run_cmd(
3609@@ -241,8 +234,14 @@
3610 result.add_result(cmd_result)
3611
3612 def run(self):
3613- """
3614- Run the test cases; including any build, setup, and cleanup commands.
3615+ """Run the complete test suite.
3616+
3617+ This includes any build, setup, and cleanup commands, as well as all
3618+ test cases (including build, setup, and cleanup.)
3619+
3620+ :returns: Whether to keep running tests (True) or reboot (False)
3621+ :rtype: bool
3622+
3623 """
3624
3625 # we gather this info from the testcases
3626@@ -291,9 +290,7 @@
3627 return keep_going
3628
3629 def add_test(self, tests):
3630- """
3631- Add a single test or list of tests to this suite.
3632- """
3633+ """Add a single test or list of tests to this suite."""
3634 if isinstance(tests, TestCase):
3635 self.tests.append(tests)
3636 else:
3637@@ -304,16 +301,17 @@
3638 pass
3639
3640 def count_tests(self):
3641+ """Return the number of test cases in the suite."""
3642 return len(self.tests)
3643
3644 def load_state(self, state):
3645- """
3646- Restore our state from the supplied dictionary.
3647+ """Restore our state from the supplied dictionary.
3648
3649 Requires that the fieldnames in the dictionary match the class
3650 properties.
3651+
3652 """
3653- # XXX: Should this be done explicitly?
3654+ # TODO: do this explicitly
3655 self.__dict__.update(state)
3656
3657 self.tests = []
3658@@ -326,6 +324,7 @@
3659 self.tests.append(test)
3660
3661 def save_state(self):
3662+ """Return a dictionary representing the suite's state."""
3663 state = {
3664 'name': self.name,
3665 'status': self.status,
3666@@ -345,20 +344,23 @@
3667 return state
3668
3669 def is_done(self):
3670- """
3671- Determine if the suite is done. This might mean that something has
3672- failed. Used by Runner to determine if the suite needs to be re-run
3673- on resume.
3674+ """Determine if the suite is done.
3675+
3676+ This might mean that something has failed.
3677+ Used by Runner to determine if the suite needs to be re-run on resume.
3678+
3679+ :returns: Whether the test suite is finished (done or cleaned up)
3680+ :rtype: bool
3681+
3682 """
3683 return self.status == 'DONE' or self.status == 'CLEANUP'
3684
3685 def get_next_test(self):
3686- """
3687- Return the next test to be run.
3688+ """Return the next test to be run.
3689
3690 Mainly used for debugging.
3691+
3692 """
3693-
3694 test = None
3695
3696 for t in self.tests:
3697
3698=== added file 'utah/client/vcs.py'
3699--- utah/client/vcs.py 1970-01-01 00:00:00 +0000
3700+++ utah/client/vcs.py 2013-04-08 19:17:22 +0000
3701@@ -0,0 +1,144 @@
3702+# Ubuntu Testing Automation Harness
3703+# Copyright 2012 Canonical Ltd.
3704+
3705+# This program is free software: you can redistribute it and/or modify it
3706+# under the terms of the GNU General Public License version 3, as published
3707+# by the Free Software Foundation.
3708+
3709+# This program is distributed in the hope that it will be useful, but
3710+# WITHOUT ANY WARRANTY; without even the implied warranties of
3711+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3712+# PURPOSE. See the GNU General Public License for more details.
3713+
3714+# You should have received a copy of the GNU General Public License along
3715+# with this program. If not, see <http://www.gnu.org/licenses/>.
3716+
3717+"""UTAH client VCS support."""
3718+
3719+
3720+from utah.client.common import (
3721+ CMD_TS_FETCH,
3722+ run_cmd,
3723+)
3724+
3725+
3726+REPO_DEFAULT_DIR = "."
3727+
3728+
3729+class VCSHandler(object):
3730+
3731+ """Base class for Version Control System support.
3732+
3733+ :param repo: Repository location
3734+ :type repo: str
3735+ :param destination:
3736+ Local directory where the repository should be made available
3737+ :type destination: str
3738+ :param battery_measurements:
3739+ Whether battery information should be gathered when running any of the
3740+ VCS commands
3741+ :type battery_measurements: bool
3742+
3743+ """
3744+
3745+ def __init__(self, repo, destination='', battery_measurements=False):
3746+ self.repo = repo
3747+ self.destination = destination
3748+ self.battery_measurements = battery_measurements
3749+
3750+ def get(self, directory=REPO_DEFAULT_DIR):
3751+ """Execute VCS get command.
3752+
3753+ The get command will make a copy of a given repository to the directory
3754+ specified as destination.
3755+
3756+ :param directory: Working directory
3757+ :type directory: str
3758+ :returns: Get command execution result
3759+ :rtype: dict
3760+
3761+ .. seealso:: :func:`run_cmd`
3762+
3763+ """
3764+ return run_cmd(self.get_command,
3765+ cwd=directory,
3766+ cmd_type=CMD_TS_FETCH,
3767+ battery_measurements=self.battery_measurements)
3768+
3769+ def revision(self, directory=REPO_DEFAULT_DIR):
3770+ """Execute revision command.
3771+
3772+ :param directory: Working directory
3773+ :type directory: str
3774+ :returns: Revision command execution result.
3775+ :rtype: dict
3776+
3777+ """
3778+ return run_cmd(self.rev_command,
3779+ cwd=directory,
3780+ cmd_type=CMD_TS_FETCH,
3781+ battery_measurements=self.battery_measurements)
3782+
3783+
3784+class BzrHandler(VCSHandler):
3785+
3786+ """Bazaar VCS handler."""
3787+
3788+ def __init__(self, repo, branch=True, options="", destination="",
3789+ **kwargs):
3790+ super(BzrHandler, self).__init__(repo, destination, **kwargs)
3791+ self.options = options
3792+
3793+ if branch:
3794+ self.get_command = "bzr branch {} {} {}".format(
3795+ options,
3796+ self.repo,
3797+ self.destination,
3798+ )
3799+
3800+ self.rev_command = "bzr revno"
3801+ else:
3802+ self.get_command = "bzr export {} {} {}".format(
3803+ options,
3804+ self.destination,
3805+ self.repo,
3806+ )
3807+
3808+ self.rev_command = "bzr revno {}".format(self.repo)
3809+
3810+
3811+class GitHandler(VCSHandler):
3812+
3813+ """Git VCS handler."""
3814+
3815+ def __init__(self, repo, options="", destination="", **kwargs):
3816+ super(GitHandler, self).__init__(repo, destination, **kwargs)
3817+ self.options = options
3818+
3819+ self.get_command = "git clone {} {} {}".format(
3820+ options,
3821+ self.repo,
3822+ self.destination,
3823+ )
3824+
3825+ self.rev_command = "git rev-parse HEAD"
3826+
3827+
3828+class DevHandler(VCSHandler):
3829+
3830+ """Development VCS handler.
3831+
3832+ This isn't a handler for any VCS, but a helper to ease development and let
3833+ runlists point to a path that can be copied locally.
3834+
3835+ """
3836+
3837+ def __init__(self, repo, destination=".", **kwargs):
3838+ super(DevHandler, self).__init__(repo, destination, **kwargs)
3839+
3840+ self.get_command = "cp -r {} {}".format(
3841+ self.repo,
3842+ self.destination,
3843+ )
3844+
3845+ self.rev_command = "echo 'DEVELOPMENT'"
3846
3847=== modified file 'utah/commandstr.py'
3848--- utah/commandstr.py 2012-12-03 14:02:18 +0000
3849+++ utah/commandstr.py 2013-04-08 19:17:22 +0000
3850@@ -13,19 +13,17 @@
3851 # You should have received a copy of the GNU General Public License along
3852 # with this program. If not, see <http://www.gnu.org/licenses/>.
3853
3854-"""
3855-Provide stringification for commands used by timeout and retry.
3856-"""
3857+"""Provide stringification for commands used by timeout and retry."""
3858
3859
3860 def commandstr(command, *args, **kw):
3861- """
3862- Convert a command and argument lists into a representation of that command
3863- suitable for display to a user.
3864+ """Provide a user-oriented command display.
3865+
3866 i.e. commandstr(command, arg1, arg2, kw1=1, kw2=2)
3867 should return 'command(arg1, arg2, kw1=1, kw2=2)'
3868+
3869 """
3870- msg = command.__name__ + '('
3871+ msg = '{}('.format(command.__name__)
3872 for arg in args:
3873 try:
3874 msg += arg.__name__
3875
3876=== modified file 'utah/config.py'
3877--- utah/config.py 2013-03-29 12:26:45 +0000
3878+++ utah/config.py 2013-04-08 19:17:22 +0000
3879@@ -1,4 +1,4 @@
3880-#!/usr/bin/python
3881+#!/usr/bin/env python
3882
3883 # Ubuntu Testing Automation Harness
3884 # Copyright 2012 Canonical Ltd.
3885@@ -15,10 +15,11 @@
3886 # You should have received a copy of the GNU General Public License along
3887 # with this program. If not, see <http://www.gnu.org/licenses/>.
3888
3889-"""
3890-Provide config variables via utah.config.
3891+"""Provide config variables via utah.config.
3892+
3893 When run directly as part of the package build, output machine-independent
3894 defaults as json for writing a config file.
3895+
3896 """
3897
3898 import getpass
3899@@ -36,9 +37,10 @@
3900
3901
3902 def getbridge():
3903- """
3904- Get virbr0 if it is set in the qemu config or the utah config.
3905+ """Get virbr0 if it is set in the qemu config or the utah config.
3906+
3907 Use this (or the default virbr0) as the bridge for bridged network VMs.
3908+
3909 """
3910 dns = os.path.join('/', 'etc', 'utah', 'dns')
3911 if os.path.isfile(dns):
3912@@ -235,13 +237,12 @@
3913
3914 # These depend on other config options, so they're added last.
3915 # Default logfile is /var/log/utah/{hostname}.log
3916-LOCALDEFAULTS['logfile'] = os.path.join(DEFAULTS['logpath'],
3917- LOCALDEFAULTS['hostname'] + '.log')
3918+LOCALDEFAULTS['logfile'] = os.path.join(
3919+ DEFAULTS['logpath'], '{}.log'.format(LOCALDEFAULTS['hostname']))
3920 # Default logfile is /var/log/utah/{hostname}-debug.log
3921 # Set to None to disable separate debug log
3922 LOCALDEFAULTS['debuglog'] = os.path.join(
3923- DEFAULTS['logpath'],
3924- LOCALDEFAULTS['hostname'] + '-debug.log')
3925+ DEFAULTS['logpath'], '{}-debug.log'.format(LOCALDEFAULTS['hostname']))
3926
3927 # Log file used to log SSH related information
3928 LOCALDEFAULTS['ssh_logfile'] = \
3929@@ -314,7 +315,8 @@
3930 try:
3931 CONFIG.update(json.load(fp))
3932 except ValueError:
3933- sys.stderr.write(conffile + ' is not a valid JSON file')
3934+ sys.stderr.write('{} is not a valid JSON file'
3935+ .format(conffile))
3936
3937
3938 # Support ~-based paths on variables that use paths
3939
3940=== modified file 'utah/exceptions.py'
3941--- utah/exceptions.py 2013-03-15 14:59:35 +0000
3942+++ utah/exceptions.py 2013-04-08 19:17:22 +0000
3943@@ -13,16 +13,17 @@
3944 # You should have received a copy of the GNU General Public License along
3945 # with this program. If not, see <http://www.gnu.org/licenses/>.
3946
3947-"""
3948-Provide exceptions for the rest of the tool.
3949-"""
3950+"""Provide exceptions for UTAH."""
3951
3952
3953 class UTAHException(Exception):
3954- """
3955- Provide a foundation class for UTAH exceptions.
3956- Support retry argument, but default to False.
3957- """
3958+
3959+ """Provide a foundation class for UTAH exceptions.
3960+
3961+ Support retry and external arguments, but default to False.
3962+
3963+ """
3964+
3965 def __init__(self, *args, **kw):
3966 self.retry = kw.pop('retry', False)
3967 self.external = kw.pop('external', False)
3968
3969=== modified file 'utah/group.py'
3970--- utah/group.py 2012-12-03 14:02:18 +0000
3971+++ utah/group.py 2013-04-08 19:17:22 +0000
3972@@ -13,9 +13,9 @@
3973 # You should have received a copy of the GNU General Public License along
3974 # with this program. If not, see <http://www.gnu.org/licenses/>.
3975
3976-"""
3977-Unix group checking functionality
3978-"""
3979+"""Provide unix group checking functionality."""
3980+
3981+
3982 import grp
3983 import os
3984 import sys
3985@@ -25,9 +25,7 @@
3986
3987
3988 def check_user_group(group=config.group):
3989- """
3990- Return if the user is a member of the given group
3991- """
3992+ """Return whether the user is a member of the given group."""
3993 user_gids = os.getgroups()
3994 gid = grp.getgrnam(group).gr_gid
3995
3996@@ -35,16 +33,14 @@
3997
3998
3999 def print_group_error_message(script):
4000- """
4001- Print error message to stderr to be used by scripts
4002- """
4003+ """Print error message to stderr to be used by scripts."""
4004 argv = list(sys.argv)
4005 argv[0] = os.path.abspath(script)
4006 message = ["Error: you are not in the utah group.",
4007 ("If you believe you have properly configured "
4008 "your user account for UTAH use, try:"),
4009- ' sudo usermod -a -G utah ' + getpass.getuser(),
4010+ ' sudo usermod -a -G utah {}'.format(getpass.getuser()),
4011 "Otherwise, please run this script as the utah user, i.e.:",
4012- " sudo su - utah -c '" + ' '.join(argv) + "'\n"]
4013+ " sudo su - utah -c '{}'\n".format(' '.join(argv))]
4014 message = '\n'.join(message)
4015 sys.stderr.write(message)
4016
4017=== modified file 'utah/iso.py'
4018--- utah/iso.py 2013-03-20 19:17:23 +0000
4019+++ utah/iso.py 2013-04-08 19:17:22 +0000
4020@@ -33,10 +33,13 @@
4021
4022
4023 class UTAHISOException(UTAHException):
4024+
4025+ """Provide an exception specific to ISO errors."""
4026+
4027 pass
4028
4029
4030-def get_resource(method, url, *args, **kw):
4031+def _get_resource(method, url, *args, **kw):
4032 try:
4033 resource = method(url, *args, **kw)
4034 return resource
4035@@ -51,9 +54,9 @@
4036
4037
4038 class ISO(object):
4039- """
4040- Provide a simplified method of interfacing with images.
4041- """
4042+
4043+ """Provide a simplified method of interfacing with images."""
4044+
4045 def __init__(self, arch=None, dlpercentincrement=None, dlretries=None,
4046 image=None, installtype=None, logger=None, series=None):
4047 if dlpercentincrement is None:
4048@@ -69,22 +72,21 @@
4049 else:
4050 self.logger = logger
4051 if image is None:
4052- self.logger.debug('Need to retrieve image')
4053 path = self.downloadiso(arch=arch, installtype=installtype,
4054 series=series)
4055 else:
4056 if image.startswith('~'):
4057 path = os.path.expanduser(image)
4058- self.logger.debug('Rewriting ~-based path: ' + image +
4059- ' to: ' + path)
4060+ self.logger.debug('Rewriting ~-based path: %s to: %s', image,
4061+ path)
4062 else:
4063 path = image
4064
4065 self.percent = 0
4066- self.logger.info('Preparing image: ' + path)
4067- self.image = get_resource(urllib.urlretrieve, path,
4068- reporthook=self.dldisplay)[0]
4069- self.logger.debug(path + ' is locally available as ' + self.image)
4070+ self.logger.info('Preparing image: %s', path)
4071+ self.image = _get_resource(urllib.urlretrieve, path,
4072+ reporthook=self.dldisplay)[0]
4073+ self.logger.info('%s is locally available as %s', path, self.image)
4074 self.installtype = self.getinstalltype()
4075 if self.installtype == 'mini':
4076 self.logger.debug('Using mini image')
4077@@ -104,9 +106,7 @@
4078 self.buildnumber = self.getbuildnumber()
4079
4080 def _loggersetup(self):
4081- """
4082- Initialize the logging for the image.
4083- """
4084+ """Initialize the logging for the image."""
4085 self.logger = logging.getLogger('utah.iso')
4086 self.logger.propagate = False
4087 self.logger.setLevel(logging.INFO)
4088@@ -119,8 +119,8 @@
4089 self.consolehandler.setLevel(config.consoleloglevel)
4090 self.logger.addHandler(self.consolehandler)
4091 self.filehandler = logging.handlers.WatchedFileHandler(config.logfile)
4092- file_formatter = logging.Formatter('%(asctime)s iso '
4093- + ' %(levelname)s: %(message)s')
4094+ file_formatter = logging.Formatter(
4095+ '%(asctime)s iso %(levelname)s: %(message)s')
4096 self.filehandler.setFormatter(file_formatter)
4097 self.filehandler.setLevel(config.fileloglevel)
4098 self.logger.addHandler(self.filehandler)
4099@@ -133,13 +133,16 @@
4100 self.logger.addHandler(self.debughandler)
4101
4102 def getinstalltype(self):
4103- """
4104- Inspect the image's files to get the image type.
4105+ """Inspect the image's files to get the image type.
4106+
4107 If .disk/mini-info exists, it's mini.
4108 If the casper directory exists, it's desktop.
4109 If ubuntu-server.seed exists in the preseeds directory, it's server.
4110+ :returns: Image type
4111+ :rtype: str
4112+
4113 """
4114- self.logger.info('Getting image type of ' + self.image)
4115+ self.logger.info('Getting image type of %s', self.image)
4116 files = set(self.listfiles(returnlist=True))
4117 installtype = 'alternate'
4118 if '.disk/mini-info' in files:
4119@@ -149,53 +152,76 @@
4120 elif ('preseed/ubuntu-server.seed' in files or
4121 './preseed/ubuntu-server.seed' in files):
4122 installtype = 'server'
4123- self.logger.info('Image type is: ' + installtype)
4124+ self.logger.info('Image type is: %s', installtype)
4125 return installtype
4126
4127 def getarch(self):
4128- """
4129- Unpack the image's info file to get the arch.
4130+ """Unpack the image's info file to get the arch.
4131+
4132+ :returns: Image architecture
4133+ :rtype: str
4134+
4135 """
4136 arch = self.media_info.split()[-2]
4137- self.logger.info('Arch is: ' + arch)
4138+ self.logger.info('Arch is: %s', arch)
4139 return arch
4140
4141 def getseries(self):
4142- """
4143- Unpack the image's info file to get the series.
4144+ """Unpack the image's info file to get the series.
4145+
4146+ :returns: Image series
4147+ :rtype: str
4148+
4149 """
4150 for word in self.media_info.split():
4151 if word.startswith('"'):
4152 series = word.strip('"').lower()
4153 break
4154- self.logger.info('Series is ' + series)
4155+ self.logger.info('Series is %s', series)
4156 return series
4157
4158 def getbuildnumber(self):
4159- """Unpack the image's info file to get the build number."""
4160+ """Unpack the image's info file to get the build number.
4161+
4162+ :returns: Build number of the image.
4163+ :rtype: str
4164+
4165+ """
4166 match = re.search('.*\(([0-9.]+)\)$', self.media_info)
4167 build_number = match.group(1) if match else '?'
4168 return build_number
4169
4170 def dldisplay(self, blocks, size, total):
4171- """
4172- Display download information.
4173- Intended for use as a urllib callback.
4174+ """Log download information (i.e., as a urllib callback).
4175+
4176+ :param blocks: Number of blocks downloaded
4177+ :type blocks: int
4178+ :param size: Size of blocks downloaded
4179+ :type size: int
4180+ :param total: Total size of download
4181+ :type size: int
4182+
4183 """
4184 read = blocks * size
4185 percent = 100 * read / total
4186 if percent >= self.percent:
4187- self.logger.info('File ' + str(percent) + '% downloaded')
4188+ self.logger.info('File %s%% downloaded', percent)
4189 self.percent += self.dlpercentincrement
4190- self.logger.debug("%s read, %s%% of %s total" % (read, percent, total))
4191+ self.logger.debug('%s read, %s%% of %s total', read, percent, total)
4192
4193 def listfiles(self, returnlist=False):
4194- """
4195+ """Return the contents of the ISO.
4196+
4197 Return either a subprocess instance listing the contents of an ISO, or
4198 a list of files in the ISO if returnlist is True.
4199+ :returns: The contents of the ISO
4200+ :rtype: list or object
4201+
4202 """
4203+ # TODO: See if we still need this outside of validation
4204+ # move it to validation if it's only needed there
4205 cmd = ['bsdtar', '-t', '-f', self.image]
4206- self.logger.debug('bsdtar list command: ' + ' '.join(cmd))
4207+ self.logger.debug('bsdtar list command: %s', ' '.join(cmd))
4208 if returnlist:
4209 try:
4210 proc = subprocess.check_output(cmd).strip().split('\n')
4211@@ -223,7 +249,7 @@
4212
4213 """
4214 cmd = ['bsdtar', '-t', '-v', '-f', self.image, filename]
4215- self.logger.debug('bsdtar list command: ' + ' '.join(cmd))
4216+ self.logger.debug('bsdtar list command: %s', ' '.join(cmd))
4217 try:
4218 output = subprocess.check_output(cmd)
4219 except (OSError, subprocess.CalledProcessError) as err:
4220@@ -237,7 +263,7 @@
4221 'for {} in {}: {}'
4222 .format(filename, self.image, output))
4223 cmd = ['bsdtar', '-x', '-f', self.image, '-O', realfile]
4224- self.logger.debug('bsdtar extract command: ' + ' '.join(cmd))
4225+ self.logger.debug('bsdtar extract command: %s', ' '.join(cmd))
4226 return cmd
4227
4228 def extract(self, filename, outdir='', outfile=None, **kw):
4229@@ -293,25 +319,39 @@
4230
4231 return stdout
4232
4233- def getmd5(self, path):
4234- """
4235- Return the md5 checksum of a file.
4236+ def getmd5(self, path=None):
4237+ """Return the MD5 checksum of a file.
4238+
4239 Default file is this image.
4240+ :param path: Path of file to checksum
4241+ :type path: str
4242+ :returns: MD5 checksum of file
4243+ :rtype: str
4244+
4245 """
4246 if path is None:
4247 path = self.image
4248- self.logger.debug('Getting md5 of ' + path)
4249+ self.logger.debug('Getting md5 of %s', path)
4250 isohash = md5()
4251 with open(path) as myfile:
4252 for block in iter(lambda: myfile.read(128), ""):
4253 isohash.update(block)
4254 filemd5 = isohash.hexdigest()
4255- self.logger.debug('md5 of ' + path + ' is ' + filemd5)
4256+ self.logger.debug('md5 of %s is %s', path, filemd5)
4257 return filemd5
4258
4259 def downloadiso(self, arch=None, installtype=None, series=None):
4260- """
4261- Download an ISO given series, type, and arch.
4262+ """Download an ISO given series, type, and arch.
4263+
4264+ :param arch: Architecture of image to download
4265+ :type arch: str
4266+ :param installtype: Install type of image to download
4267+ :type installtype: str
4268+ :param series: Series codename of image to download
4269+ :type series: str
4270+ :returns: Local path of downloaded image
4271+ :rtype: str
4272+
4273 """
4274 if arch is None:
4275 arch = config.arch
4276@@ -319,26 +359,26 @@
4277 installtype = config.installtype
4278 if series is None:
4279 series = config.series
4280- filename = '-'.join([series, installtype, arch]) + '.iso'
4281- self.logger.info('Attempting to retrieve ' + filename)
4282+ filename = '{}.iso'.format('-'.join([series, installtype, arch]))
4283+ self.logger.info('Attempting to retrieve %s', filename)
4284 with open(os.devnull, "w") as fnull:
4285 # If dlcommand (default dl-ubuntu-test-iso) is available, use it
4286 cmd = ['which', config.dlcommand]
4287 try:
4288 if subprocess.call(cmd,
4289 stdout=fnull, stderr=fnull) == 0:
4290- self.logger.debug('Using ' + config.dlcommand)
4291+ self.logger.debug('Using %s', config.dlcommand)
4292 if installtype == 'server':
4293 flavor = 'ubuntu-server'
4294 else:
4295 flavor = 'ubuntu'
4296 cmd = [config.dlcommand,
4297 '-q',
4298- '--flavor=' + flavor,
4299- '--release=' + series,
4300- '--variant=' + installtype,
4301- '--arch=' + arch,
4302- '--isoroot=' + config.isodir]
4303+ '--flavor={}'.format(flavor),
4304+ '--release={}'.format(series),
4305+ '--variant={}'.format(installtype),
4306+ '--arch={}'.format(arch),
4307+ '--isoroot={}'.format(config.isodir)]
4308 self.logger.info('Downloading ISO')
4309 self.logger.debug(' '.join(cmd))
4310 try:
4311@@ -361,7 +401,7 @@
4312 # First, we'll check the image. If it matches, we return
4313 # If not, we'll download it, and start the loop over
4314 for attempt in range(config.dlretries):
4315- self.logger.info('Download attempt ' + str(attempt))
4316+ self.logger.info('Download attempt %s', str(attempt))
4317 # Set the path to our files and the name of the iso we want
4318 if installtype == 'mini':
4319 remotepath = ('http://archive.ubuntu.com/ubuntu/dists/'
4320@@ -371,39 +411,34 @@
4321 else:
4322 remotepath = ('http://releases.ubuntu.com/'
4323 '{series}/'.format(series=series))
4324- isopattern = installtype + '-' + arch + '.iso'
4325+ isopattern = '{}-{}.iso'.format(installtype, arch)
4326
4327- # Scan the MD5SUMS file for our file
4328 servermd5 = None
4329 md5path = '{}/MD5SUMS'.format(remotepath)
4330- md5list = get_resource(urllib.urlopen, md5path)
4331+ md5list = _get_resource(urllib.urlopen, md5path)
4332 for line in md5list:
4333 if isopattern in line:
4334 servermd5, isofile = line.split()
4335 isofile = isofile.strip('*')
4336 break
4337
4338- # If we can't find the ISO, raise an exception
4339 if servermd5 is None:
4340 raise UTAHISOException(
4341 'Specified ISO: {} not found on mirrors.'
4342 .format(filename), external=True)
4343- self.logger.debug('Server md5 is ' + servermd5)
4344+ self.logger.debug('Server md5 is %s', servermd5)
4345
4346- # If the ISO exists locally and the md5 matches, use it
4347 if os.path.isfile(path) and servermd5 == self.getmd5(path):
4348- self.logger.info('Using ISO at ' + path)
4349+ self.logger.info('Using ISO at %s', path)
4350 return path
4351
4352- # Try to download the ISO
4353 isopath = os.path.join(remotepath, isofile)
4354 self.percent = 0
4355- self.logger.info('Attempting to download ' + isopath)
4356- temppath = get_resource(urllib.urlretrieve, isopath,
4357- reporthook=self.dldisplay)[0]
4358+ self.logger.info('Attempting to download %s', isopath)
4359+ temppath = _get_resource(urllib.urlretrieve, isopath,
4360+ reporthook=self.dldisplay)[0]
4361
4362- # Try to copy it into our cache
4363- self.logger.debug('Copying ' + temppath + ' to ' + path)
4364+ self.logger.debug('Copying %s to %s', temppath, path)
4365 shutil.copyfile(temppath, path)
4366 else:
4367 if os.path.isfile(path) and servermd5 == self.getmd5(path):
4368@@ -414,6 +449,12 @@
4369 .format(config.dlretries), external=True)
4370
4371 def kernelpath(self):
4372+ """Return the path of the kernel inside the image.
4373+
4374+ :returns: Path to the kernel pulled from bootloader config
4375+ :rtype: str
4376+
4377+ """
4378 kernelpath = './install/vmlinuz'
4379 if self.installtype == 'mini':
4380 self.logger.debug('Getting kernel for mini image')
4381@@ -437,8 +478,8 @@
4382 self.logger.debug('Rejecting '
4383 'memtest kernel')
4384 else:
4385- self.logger.debug('Found kernel: '
4386- + newkernel)
4387+ self.logger.debug(
4388+ 'Found kernel: %s', newkernel)
4389 kernels[newkernel] += 1
4390 # Now we have a list of kernel paths and the number of times
4391 # each once occurs. We'll use the one that occurs most.
4392
4393=== modified file 'utah/isotest/iso_static_validation.py'
4394--- utah/isotest/iso_static_validation.py 2013-01-03 11:51:00 +0000
4395+++ utah/isotest/iso_static_validation.py 2013-04-08 19:17:22 +0000
4396@@ -1,4 +1,4 @@
4397-#!/usr/bin/python
4398+#!/usr/bin/env python
4399
4400 # Ubuntu Testing Automation Harness
4401 # Copyright 2012 Canonical Ltd.
4402@@ -35,14 +35,14 @@
4403 # <http://www.gnu.org/licenses/>.
4404 #
4405
4406-# TODO: support for non-daily images
4407-
4408 """ISO static validation.
4409
4410 The test cases in this scripts are used to make sure that an ISO image isn't
4411 broken and can be used for smoke testing.
4412
4413 """
4414+# TODO: support for non-daily images
4415+
4416
4417 import os
4418 import argparse
4419@@ -143,14 +143,16 @@
4420 description=self.shortDescription() or ''))
4421 self.block_size = ONE_MB_BLOCK
4422 self.iso_location = iso_location
4423- logging.debug('Using iso at: ' + self.iso_location)
4424+ logging.debug('Using iso at: %s', self.iso_location)
4425 self.iso = ISO(image=self.iso_location, logger=logging)
4426 self.st_release = self.iso.series
4427 self.st_variant = self.iso.installtype
4428 self.st_arch = self.iso.arch
4429- self.iso_name = '-'.join([self.st_release, self.st_variant,
4430- self.st_arch]) + '.iso'
4431- logging.debug('Standard name for this iso is: ' + self.iso_name)
4432+ self.iso_name = ('{}.iso'
4433+ .format('-'.join([self.st_release,
4434+ self.st_variant,
4435+ self.st_arch])))
4436+ logging.debug('Standard name for this iso is: %s', self.iso_name)
4437
4438 if self.st_release != 'precise':
4439 self.url = DEFAULT_URL
4440@@ -212,15 +214,9 @@
4441
4442 def test_files_list(self):
4443 """Test if the list repository file matches the content of the ISO."""
4444- if self.st_variant == 'server':
4445- list_url = os.path.join(self.url, 'current',
4446- self.st_release + '-' + 'server' + '-' +
4447- self.st_arch + '.list')
4448- else:
4449- list_url = os.path.join(self.url, 'current',
4450- self.st_release + '-' +
4451- self.st_variant + '-' +
4452- self.st_arch + '.list')
4453+ list_url = os.path.join(
4454+ self.url, 'current', '{}.list'
4455+ .format('-'.join([self.st_release, st_variant, self.st_arch])))
4456
4457 try:
4458 list_repository = urllib2.urlopen(list_url)
4459@@ -247,10 +243,10 @@
4460 "manifest only for ubiquity based images")
4461 def test_manifest(self):
4462 """Test if the ISO manifest matches the one in the server."""
4463- manifest_url = os.path.join(self.url, 'current',
4464- self.st_release + '-' +
4465- self.st_variant + '-' +
4466- self.st_arch + '.manifest')
4467+ manifest_url = os.path.join(
4468+ self.url, 'current', '{}.manifest'
4469+ .format('-'.join([self.st_release, st_variant, self.st_arch])))
4470+
4471 try:
4472 manifest = urllib2.urlopen(manifest_url)
4473 except urllib2.HTTPError, e:
4474@@ -326,7 +322,7 @@
4475 for list_server in files_list:
4476 path = list_server.rstrip()
4477 if path in exclude_files:
4478- logging.debug(path + ' excluded based on release')
4479+ logging.debug('%s excluded based on release', path)
4480 else:
4481 self.assertIn(path, stdout)
4482
4483@@ -370,7 +366,7 @@
4484 logging.debug('check if important d-i files are present in iso')
4485 path = list_server.rstrip()
4486 if path in exclude_files:
4487- logging.debug(path + ' excluded based on release')
4488+ logging.debug('%s excluded based on release', path)
4489 else:
4490 self.assertIn(path, stdout)
4491
4492
4493=== modified file 'utah/orderedcollections.py'
4494--- utah/orderedcollections.py 2012-12-08 02:10:12 +0000
4495+++ utah/orderedcollections.py 2013-04-08 19:17:22 +0000
4496@@ -13,9 +13,7 @@
4497 # You should have received a copy of the GNU General Public License along
4498 # with this program. If not, see <http://www.gnu.org/licenses/>.
4499
4500-"""
4501-Provide additional collections to support cleanup functions.
4502-"""
4503+"""Provide additional collections to support cleanup functions."""
4504
4505
4506 import collections
4507@@ -25,6 +23,8 @@
4508
4509 class OrderedSet(collections.MutableSet):
4510
4511+ """Provide a set with ordering."""
4512+
4513 def __init__(self, iterable=None):
4514 self.end = end = []
4515 end += [None, end, end] # sentinel node for doubly linked list
4516@@ -39,12 +39,14 @@
4517 return key in self.map
4518
4519 def add(self, key):
4520+ """add an item to the set."""
4521 if key not in self.map:
4522 end = self.end
4523 curr = end[PREV]
4524 curr[NEXT] = end[PREV] = self.map[key] = [key, curr, end]
4525
4526 def discard(self, key):
4527+ """remove an item from the set."""
4528 if key in self.map:
4529 key, prev, next = self.map.pop(key)
4530 prev[NEXT] = next
4531@@ -65,6 +67,12 @@
4532 curr = curr[PREV]
4533
4534 def pop(self, last=True):
4535+ """Remove an item from the set and return that item.
4536+
4537+ :param last: Remove the last added item (instead of the first)
4538+ :type last: bool
4539+
4540+ """
4541 if not self:
4542 raise KeyError('set is empty')
4543 key = next(reversed(self)) if last else next(iter(self))
4544@@ -86,5 +94,8 @@
4545
4546
4547 class HashableDict(dict):
4548+
4549+ """Provide a hashable dict."""
4550+
4551 def __hash__(self):
4552 return hash(tuple(sorted(self.items())))
4553
4554=== modified file 'utah/parser.py'
4555--- utah/parser.py 2013-03-14 13:37:03 +0000
4556+++ utah/parser.py 2013-04-08 19:17:22 +0000
4557@@ -14,6 +14,8 @@
4558 # with this program. If not, see <http://www.gnu.org/licenses/>.
4559
4560 """UTAH Client log parser."""
4561+
4562+
4563 import argparse
4564 import jsonschema
4565 import logging
4566
4567=== modified file 'utah/preseed.py'
4568--- utah/preseed.py 2012-12-03 14:02:18 +0000
4569+++ utah/preseed.py 2013-04-08 19:17:22 +0000
4570@@ -63,6 +63,8 @@
4571 <BLANKLINE>
4572
4573 """
4574+
4575+
4576 import string
4577
4578
4579@@ -885,6 +887,8 @@
4580
4581 """
4582
4583+ pass
4584+
4585
4586 class DuplicatedQuestionName(Exception):
4587
4588@@ -904,3 +908,5 @@
4589 DuplicatedQuestionName: passwd/username
4590
4591 """
4592+
4593+ pass
4594
4595=== modified file 'utah/process.py'
4596--- utah/process.py 2013-03-29 09:00:34 +0000
4597+++ utah/process.py 2013-04-08 19:17:22 +0000
4598@@ -14,6 +14,8 @@
4599 # with this program. If not, see <http://www.gnu.org/licenses/>.
4600
4601 """Process related utilities."""
4602+
4603+
4604 import logging
4605 import subprocess
4606
4607
4608=== modified file 'utah/provisioning/__init__.py'
4609--- utah/provisioning/__init__.py 2012-12-03 14:02:18 +0000
4610+++ utah/provisioning/__init__.py 2013-04-08 19:17:22 +0000
4611@@ -13,6 +13,4 @@
4612 # You should have received a copy of the GNU General Public License along
4613 # with this program. If not, see <http://www.gnu.org/licenses/>.
4614
4615-"""
4616-utah.provisioning
4617-"""
4618+"""utah.provisioning"""
4619
4620=== modified file 'utah/provisioning/baremetal/__init__.py'
4621--- utah/provisioning/baremetal/__init__.py 2012-12-03 14:02:18 +0000
4622+++ utah/provisioning/baremetal/__init__.py 2013-04-08 19:17:22 +0000
4623@@ -13,6 +13,4 @@
4624 # You should have received a copy of the GNU General Public License along
4625 # with this program. If not, see <http://www.gnu.org/licenses/>.
4626
4627-"""
4628-utah.provisioning.baremetal
4629-"""
4630+"""utah.provisioning.baremetal"""
4631
4632=== modified file 'utah/provisioning/baremetal/bamboofeeder.py'
4633--- utah/provisioning/baremetal/bamboofeeder.py 2013-03-28 10:58:47 +0000
4634+++ utah/provisioning/baremetal/bamboofeeder.py 2013-04-08 19:17:22 +0000
4635@@ -13,9 +13,8 @@
4636 # You should have received a copy of the GNU General Public License along
4637 # with this program. If not, see <http://www.gnu.org/licenses/>.
4638
4639-"""
4640-Support bare-metal provisioning of bamboo-feeder-based systems.
4641-"""
4642+"""Support provisioning of bamboo-feeder-based systems."""
4643+
4644
4645 import os
4646 import pipes
4647@@ -36,12 +35,13 @@
4648
4649
4650 class BambooFeederMachine(CustomInstallMixin, SSHMixin, PowerMixin, Machine):
4651- """
4652- Provide a class to provision an ARM board from a bamboo-feeder setup.
4653- """
4654+
4655+ """Provision and manage an ARM board in a bamboo-feeder setup."""
4656+
4657+ # TODO: raise more exceptions if ProcessRunner fails
4658+ # maybe get some easy way to do that like failok
4659 def __init__(self, inventory=None, machineinfo=None, name=None,
4660 preboot=None, *args, **kw):
4661- # TODO: change the name of cargs here and elsewhere
4662 # TODO: respect rewrite setting
4663 if name is None:
4664 raise UTAHBMProvisioningException(
4665@@ -64,8 +64,8 @@
4666 self.logger.debug('Configuring for %s with IP %s',
4667 config.wwwiface, self.ip)
4668 self._cmdlinesetup()
4669- imageurl = 'http://' + self.ip + '/utah/' + self.name + '.img'
4670- preenvurl = 'http://' + self.ip + '/utah/' + self.name + '.preEnv'
4671+ imageurl = 'http://{}/utah/{}.img'.format(self.ip, self.name)
4672+ preenvurl = 'http://{}/utah/{}.preEnv'.format(self.ip, self.name)
4673 if preboot is None:
4674 self.preboot = ('console=ttyO2,115200n8 imageurl={imageurl} '
4675 'bootcfg={preenvurl}'.format(
4676@@ -78,20 +78,22 @@
4677 self.logger.debug('BambooFeederMachine init finished')
4678
4679 def _prepareimage(self):
4680- """
4681- Make a copy of the image shared via wwa so we can edit it.
4682+ """Make a copy of the image and share it via www.
4683+
4684+ :returns: Path to www-available image
4685+ :rtype: str
4686+
4687 """
4688 self.logger.info('Making copy of install image')
4689- self.wwwimage = os.path.join(config.wwwdir, self.name + '.img')
4690- self.logger.debug('Copying ' + self.image + ' to ' + self.wwwimage)
4691+ self.wwwimage = os.path.join(config.wwwdir,
4692+ '{}.img'.format(self.name))
4693+ self.logger.debug('Copying %s to %s', self.image, self.wwwimage)
4694 self.cleanfile(self.wwwimage)
4695 shutil.copyfile(self.image, self.wwwimage)
4696 return self.wwwimage
4697
4698 def _mountimage(self):
4699- """
4700- Mount an ARM boot image so we can edit it.
4701- """
4702+ """Mount an ARM boot image."""
4703 self.logger.info('Mounting install image')
4704 self.imagedir = os.path.join(self.tmpdir, 'image.d')
4705 if not os.path.isdir(self.imagedir):
4706@@ -102,28 +104,24 @@
4707 except (OSError, subprocess.CalledProcessError) as err:
4708 raise UTAHBMProvisioningException('Failed to get image info: {}'
4709 .format(err))
4710- self.logger.debug('Image start sector is ' + str(self.sector))
4711+ self.logger.debug('Image start sector is %s', str(self.sector))
4712 self.cleanfunction(self._umountimage)
4713- self.logger.debug('Mounting ' + self.wwwimage + ' at ' +
4714- self.imagedir)
4715+ self.logger.debug('Mounting %s at %s', self.wwwimage, self.imagedir)
4716 ProcessRunner(['sudo', 'mount', self.wwwimage, self.imagedir,
4717- '-o', 'loop', '-o', 'offset=' + str(self.sector * 512),
4718- '-o', 'uid=' + config.user])
4719+ '-o', 'loop',
4720+ '-o', 'offset={}'.format(str(self.sector * 512)),
4721+ '-o', 'uid={}'.format(config.user)])
4722
4723 def _setupconsole(self):
4724- """
4725- Setup the install to use a serial console."
4726- """
4727+ """Setup the install to use a serial console."""
4728 self.logger.info('Setting up the install to use the serial console')
4729 preenvfile = os.path.join(self.imagedir, 'preEnv.txt')
4730 serialpreenvfile = os.path.join(self.imagedir, 'preEnv.txt-serial')
4731- self.logger.debug('Copying ' + serialpreenvfile + ' to ' + preenvfile)
4732+ self.logger.debug('Copying %s to %s', serialpreenvfile, preenvfile)
4733 shutil.copyfile(serialpreenvfile, preenvfile)
4734
4735 def _unpackinitrd(self):
4736- """
4737- Unpack the uInitrd file into a directory so we can modify it.
4738- """
4739+ """Unpack the uInitrd file into a directory."""
4740 self.logger.info('Unpacking uInitrd')
4741 if not os.path.isdir(os.path.join(self.tmpdir, 'initrd.d')):
4742 os.makedirs(os.path.join(self.tmpdir, 'initrd.d'))
4743@@ -139,10 +137,10 @@
4744 raise UTAHBMProvisioningException('Failed to get initrd info: {}'
4745 .format(err))
4746 headersize = filesize - datasize
4747- self.logger.debug('uInitrd header size is ' + str(headersize))
4748+ self.logger.debug('uInitrd header size is %s', str(headersize))
4749 pipe = pipes.Template()
4750- pipe.prepend('dd if=$IN bs=1 skip=' + str(headersize) +
4751- ' 2>/dev/null', 'f-')
4752+ pipe.prepend('dd if=$IN bs=1 skip={} 2>/dev/null'
4753+ .format(str(headersize)), 'f-')
4754 pipe.append('gunzip', '--')
4755 pipe.append('cpio -ivd 2>/dev/null', '-.')
4756 exitstatus = pipe.copy(self.initrd, '/dev/null')
4757@@ -153,9 +151,7 @@
4758 'Unpacking initrd exited with status {}'.format(exitstatus))
4759
4760 def _repackinitrd(self):
4761- """
4762- Repack the uInitrd file from the initrd.d directory.
4763- """
4764+ """Repack the uInitrd file from the initrd.d directory."""
4765 self.logger.info('Repacking uInitrd')
4766 os.chdir(os.path.join(self.tmpdir, 'initrd.d'))
4767 pipe = pipes.Template()
4768@@ -181,44 +177,40 @@
4769 self.initrd])
4770
4771 def _umountimage(self):
4772- """
4773- Unmount the image after we're done with it."
4774- """
4775+ """Unmount the image after we're done with it."""
4776 self.logger.info('Unmounting image')
4777 ProcessRunner(['sudo', 'umount', self.imagedir])
4778
4779 def _cmdlinesetup(self, boot=None):
4780- """
4781- Add the needed options to the command line for an automatic install.
4782- """
4783+ """Add options to the command line for an automatic install."""
4784 if boot is None:
4785 boot = self.boot
4786 super(BambooFeederMachine, self)._cmdlinesetup(boot=boot)
4787 # TODO: minimize these
4788 for option in ('auto', 'ro', 'text'):
4789 if option not in self.cmdline:
4790- self.logger.info('Adding boot option: ' + option)
4791- self.cmdline += ' ' + option
4792- for parameter in (('cdrom-detect/try_usb', 'true'),
4793- ('console', 'ttyO2,115200n8'),
4794- ('country', 'US'),
4795- ('hostname', self.name),
4796- ('language', 'en'),
4797- ('locale', 'en_US'),
4798- ('loghost', self.ip),
4799- ('log_port', '10514'),
4800- ('netcfg/choose_interface', 'auto'),
4801- ('url', 'http://' + self.ip + '/utah/' + self.name +
4802- '.cfg')):
4803+ self.logger.info('Adding boot option: %s', option)
4804+ self.cmdline += ' {}'.format(option)
4805+ for parameter in (
4806+ ('cdrom-detect/try_usb', 'true'),
4807+ ('console', 'ttyO2,115200n8'),
4808+ ('country', 'US'),
4809+ ('hostname', self.name),
4810+ ('language', 'en'),
4811+ ('locale', 'en_US'),
4812+ ('loghost', self.ip),
4813+ ('log_port', '10514'),
4814+ ('netcfg/choose_interface', 'auto'),
4815+ ('url', 'http://{}/utah/{}.cfg'.format(self.ip, self.name)),
4816+ ):
4817 if parameter[0] not in self.cmdline:
4818- self.logger.info('Adding boot option: ' + '='.join(parameter))
4819- self.cmdline += ' ' + '='.join(parameter)
4820+ self.logger.info('Adding boot option: %s',
4821+ '='.join(parameter))
4822+ self.cmdline += ' {}'.format('='.join(parameter))
4823 self.cmdline.strip()
4824
4825 def _configurepxe(self):
4826- """
4827- Setup PXE configuration to boot remote image.
4828- """
4829+ """Setup PXE configuration to boot remote image."""
4830 # TODO: Maybe move this into pxe.py
4831 # TODO: look into cutting out the middleman/
4832 # booting straight into the installer? (maybe nfs?)
4833@@ -234,37 +226,36 @@
4834 """.format(preboot=self.preboot)
4835 tmppxefile = os.path.join(self.tmpdir, 'pxe')
4836 open(tmppxefile, 'w').write(pxeconfig)
4837- self.logger.debug('PXE info written to ' + tmppxefile)
4838- pxefile = os.path.join(config.pxedir,
4839- '01-' + self.macaddress.replace(':', '-'))
4840+ self.logger.debug('PXE info written to %s', tmppxefile)
4841+ pxefile = os.path.join(
4842+ config.pxedir, '01-{}'.format(self.macaddress.replace(':', '-')))
4843 self.cleancommand(('sudo', 'rm', '-f', pxefile))
4844- self.logger.debug('Copying ' + tmppxefile + ' to ' + pxefile)
4845+ self.logger.debug('Copying %s to %s', tmppxefile, pxefile)
4846 ProcessRunner(['sudo', 'cp', tmppxefile, pxefile])
4847- preenvfile = os.path.join(config.wwwdir, self.name + '.preEnv')
4848+ preenvfile = os.path.join(config.wwwdir,
4849+ '{}.preEnv'.format(self.name))
4850 # TODO: sort this out with self.boot
4851 # figure out which one should be what and customizable
4852- self.preenv = 'bootargs=' + self.cmdline
4853+ self.preenv = 'bootargs={}'.format(self.cmdline)
4854 self.logger.debug('Preenv setup:')
4855 self.logger.debug(self.preenv)
4856 self.cleanfile(preenvfile)
4857- self.logger.debug('Writing preenv setup to ' + preenvfile)
4858+ self.logger.debug('Writing preenv setup to %s', preenvfile)
4859 open(preenvfile, 'w').write(self.preenv)
4860
4861 def _create(self):
4862- """
4863- Install the OS on the system.
4864- """
4865+ """Install the OS on the system."""
4866 # TODO: more checks and exceptions for failures
4867 self.logger.info('Preparing system install')
4868- self.tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_')
4869+ self.tmpdir = tempfile.mkdtemp(prefix='/tmp/{}_'.format(self.name))
4870 self.cleanfile(self.tmpdir)
4871- self.logger.debug('Using ' + self.tmpdir + ' as temp dir')
4872+ self.logger.debug('Using %s as temp dir', self.tmpdir)
4873 os.chdir(self.tmpdir)
4874 self._prepareimage()
4875 self._mountimage()
4876 self._setupconsole()
4877 self.initrd = os.path.join(self.imagedir, 'uInitrd')
4878- self.logger.debug('uInitrd is ' + self.initrd)
4879+ self.logger.debug('uInitrd is %s', self.initrd)
4880 self._unpackinitrd()
4881 self._setuplatecommand()
4882 # TODO: check if this is still needed
4883@@ -278,7 +269,7 @@
4884 myfile.close()
4885 self._setuppreseed()
4886 self.logger.debug('Copying preseed to download location')
4887- preseedfile = os.path.join(config.wwwdir, self.name + '.cfg')
4888+ preseedfile = os.path.join(config.wwwdir, '{}.cfg'.format(self.name))
4889 self.cleanfile(preseedfile)
4890 shutil.copyfile(os.path.join(self.tmpdir, 'initrd.d', 'preseed.cfg'),
4891 preseedfile)
4892@@ -300,12 +291,9 @@
4893 self.cleanfunction(self.run, (
4894 'dd', 'bs=512k', 'count=10', 'if=/dev/zero', 'of=/dev/mmcblk0'),
4895 root=True)
4896- return True
4897
4898 def _depcheck(self):
4899- """
4900- Check for dependencies that are in Recommends or Suggests.
4901- """
4902+ """Check for dependencies that are in Recommends or Suggests."""
4903 super(BambooFeederMachine, self)._depcheck()
4904 cmd = ['which', 'mkimage']
4905 if ProcessRunner(cmd).returncode != 0:
4906
4907=== modified file 'utah/provisioning/baremetal/cobbler.py'
4908--- utah/provisioning/baremetal/cobbler.py 2013-03-29 15:55:46 +0000
4909+++ utah/provisioning/baremetal/cobbler.py 2013-04-08 19:17:22 +0000
4910@@ -13,9 +13,8 @@
4911 # You should have received a copy of the GNU General Public License along
4912 # with this program. If not, see <http://www.gnu.org/licenses/>.
4913
4914-"""
4915-Support bare metal provisioning through cobbler.
4916-"""
4917+"""Support bare metal provisioning through cobbler."""
4918+
4919
4920 import os
4921 import pipes
4922@@ -35,9 +34,11 @@
4923
4924
4925 class CobblerMachine(CustomInstallMixin, SSHMixin, PowerMixin, Machine):
4926- """
4927- Provide a class to provision a machine via cobbler.
4928- """
4929+
4930+ """Provision and manage a machine via cobbler."""
4931+
4932+ # TODO: raise more exceptions if ProcessRunner fails
4933+ # maybe get some easy way to do that like failok
4934 def __init__(self, inventory=None, machineinfo=None,
4935 name=None, *args, **kw):
4936 # TODO: support for reusing existing machines
4937@@ -73,27 +74,22 @@
4938 self.carch = 'x86_64'
4939 else:
4940 self.carch = self.arch
4941- self.cname = self.name + '-' + self.carch
4942- self.logger.debug('Cobbler arch is ' + self.carch)
4943+ self.cname = '-'.join([self.name, self.carch])
4944+ self.logger.debug('Cobbler arch is %s', self.carch)
4945 self.logger.debug('Cobbler machine init finished')
4946
4947 def _load(self):
4948- """
4949- Verify the machine is in cobbler.
4950- """
4951- # TODO: consider reworking _cobble to provide this
4952- # (only if we'll be using cobbler for a while longer)
4953+ """Verify the machine is in cobbler."""
4954 machines = self._cobble(['system', 'find'])['output'].splitlines()
4955 if self.name not in machines:
4956 raise UTAHBMProvisioningException(
4957 'No machine named {} exists in cobbler'.format(self.name))
4958+ else:
4959+ self.provisioned = True
4960
4961- def _create(self, checktimeout=config.checktimeout,
4962- installtimeout=config.installtimeout):
4963- """
4964- Install a machine.
4965- """
4966- self.tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_')
4967+ def _create(self):
4968+ """Install the OS on the machine."""
4969+ self.tmpdir = tempfile.mkdtemp(prefix='/tmp/{}_'.format(self.name))
4970 self.cleanfile(self.tmpdir)
4971 os.chmod(self.tmpdir, 0775)
4972
4973@@ -134,7 +130,7 @@
4974 if self.rewrite == 'all':
4975 self._setuplogging(tmpdir=self.tmpdir)
4976 else:
4977- self.logger.debug('Skipping logging setup because rewrite is' +
4978+ self.logger.debug('Skipping logging setup because rewrite is %s',
4979 self.rewrite)
4980
4981 initrd = self._repackinitrd()
4982@@ -146,27 +142,34 @@
4983
4984 if self.installtype in ['alternate', 'server']:
4985 self.logger.info('Importing image')
4986- self._cobble(['import', '--name=' + self.cname,
4987- '--path=' + self.isodir, '--arch=' + self.carch])
4988- self._cobble(['distro', 'edit', '--name=' + self.cname,
4989- '--kernel=' + kernel, '--initrd=' + initrd])
4990+ self._cobble(['import',
4991+ '--name={}'.format(self.cname),
4992+ '--path={}'.format(self.isodir),
4993+ '--arch={}'.format(self.carch)])
4994+ self._cobble(['distro', 'edit',
4995+ '--name={}'.format(self.cname),
4996+ '--kernel={}'.format(kernel),
4997+ '--initrd={}'.format(initrd)])
4998 elif self.installtype in ['mini', 'desktop']:
4999 self.logger.info('Creating distro')
5000- self._cobble(['distro', 'add', '--name=' + self.cname,
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches