Merge lp:~nuclearbob/utah/docstring-cleanup into lp:utah
- docstring-cleanup
- Merge into dev
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andy Doan (community) | Approve | ||
Max Brustkern (community) | Needs Resubmitting | ||
Review via email:
|
Commit message
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) wrote : | # |
Looks good. How bout we add a unit test to cover the get_children_
- 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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
> Looks good. How bout we add a unit test to cover the get_children_
> change. here's something i hacked together:
>
> http://
Added that. Good idea.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) wrote : | # |
common.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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_
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) wrote : | # |
rsyslogd.py
# TODO: Find out if this can/should be None
the TODO line that was added isn't needed. This line is required and safe.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
not sure.
- 967. By Max Brustkern
-
Implemented some fixes suggested by Andy
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
>
> 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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
> 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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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_
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
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 | |
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, |
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.