Merge lp:~milo/lava-tool/lava-168 into lp:~linaro-validation/lava-tool/trunk
- lava-168
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 188 |
Proposed branch: | lp:~milo/lava-tool/lava-168 |
Merge into: | lp:~linaro-validation/lava-tool/trunk |
Prerequisite: | lp:~milo/lava-tool/lava-167 |
Diff against target: |
1306 lines (+631/-360) 15 files modified
lava/config.py (+1/-1) lava/device/commands.py (+17/-147) lava/device/tests/test_commands.py (+30/-105) lava/device/tests/test_device.py (+5/-19) lava/helper/command.py (+101/-0) lava/helper/dispatcher.py (+110/-0) lava/helper/tests/helper_test.py (+61/-0) lava/helper/tests/test_command.py (+53/-0) lava/helper/tests/test_dispatcher.py (+52/-0) lava/job/__init__.py (+2/-2) lava/job/commands.py (+83/-36) lava/job/tests/test_commands.py (+70/-49) lava_tool/tests/__init__.py (+3/-0) lava_tool/tests/test_utils.py (+41/-0) lava_tool/utils.py (+2/-1) |
To merge this branch: | bzr merge lp:~milo/lava-tool/lava-168 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Antonio Terceiro | Needs Fixing | ||
Linaro Automation & Validation | Pending | ||
Review via email:
|
Commit message
Description of the change
This branch adds the 'lava job run' command to run a job file in the local dispatcher.
[updated: resubmitted, was using wrong target and no prerequisite]
- 196. By Milo Casagrande
-
Merged latests chanes from parent branch.
- 197. By Milo Casagrande
-
Refactored the BaseCommand class.
* Moved BaseCommand in a helper package and in its own class.
* Refactored method names.
* Added a dispatcher helper class for all function calls that interact
or deal with the LAVA dispatcher. - 198. By Milo Casagrande
-
Refactored tests.
* First batch of changes.
* Added test helper function to collect setUp and tearDown methods.
* Moved BaseCommand and the new dispatcher method tests here. - 199. By Milo Casagrande
-
Refactored tests and commands.
* Use the new test helper class.
* Use the new base command class. - 200. By Milo Casagrande
-
Use the new base command class.
- 201. By Milo Casagrande
-
Use os.path.devnull.
- 202. By Milo Casagrande
-
Added test for the utils module.
- 203. By Milo Casagrande
-
Added tests to the test suite.
- 204. By Milo Casagrande
-
Added dispatcher helper tests.
- 205. By Milo Casagrande
-
Fixed command help.
- 206. By Milo Casagrande
-
Fixed tests, use new test helper class.

Milo Casagrande (milo) wrote : | # |
Hi Antonio,
thanks for going through the review!
On Tue, Jun 18, 2013 at 9:13 PM, Antonio Terceiro
<email address hidden> wrote:
> Review: Needs Fixing
>
> Hi Milo,
>
> Thanks for this. Some of my comments are related to the questions we already
> talked about earlier today, but I will also mention them here so others can
> also follow.
>
> review needs-fixing
>
>> === added file 'lava/base_
>> --- lava/base_
>> +++ lava/base_
>
> I would put this somewhere like lava/helper/
Agree.
This is already done with the latest push.
I also refactored all the calls that made use of the lava-dispatcher
into lava/helper/
> Maybe when we start working on the other items (testdef/script) we realize that
> we can move all of them in the lava.helper namespace instead of creating
> lava.testdef and lava.script.
>
>> @@ -0,0 +1,80 @@
>> +# Copyright (C) 2013 Linaro Limited
>> +#
>> +# Author: Milo Casagrande <email address hidden>
>> +#
>> +# This file is part of lava-tool.
>> +#
>> +# lava-tool is free software: you can redistribute it and/or modify
>> +# it under the terms of the GNU Lesser General Public License version 3
>> +# as published by the Free Software Foundation
>> +#
>> +# lava-tool is distributed in the hope that it will be useful,
>> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
>> +# GNU General Public License for more details.
>> +#
>> +# You should have received a copy of the GNU Lesser General Public License
>> +# along with lava-tool. If not, see <http://
>> +
>> +"""Base command class common to lava commands series."""
>> +
>> +import subprocess
>> +
>> +from lava.config import InteractiveConfig
>> +from lava.tool.command import Command
>> +from lava.tool.errors import CommandError
>> +
>> +
>> +class BaseCommand(
>> + """Base command class for all lava commands."""
>> + def __init__(self, parser, args):
>> + super(BaseCommand, self)._
>> + self.config = InteractiveConfig(
>> + force_interacti
>> +
>> + @classmethod
>> + def register_
>> + super(BaseCommand, cls).register_
>> + parser.
>> + action=
>> + help=("Forces asking for input parameters even if "
>> + "we already have them cached."))
>
> As we talked ealier, I think it's better to do it the other way around: assume
> interactivity always, and have a --non-interactive option to force not asking
> for input.
This one is done too.
> I think we should move all the user interaction asking for input to the
> config/parameter classes. This way we use the existing mechanism to store
> answers in configuration and ease users' life.
Agree here too, but I guess there will be some refactoring involved,
and the code review here might...
- 207. By Milo Casagrande
-
PEP8 fixes.
- 208. By Milo Casagrande
-
Fixed problem with one test.
- 209. By Milo Casagrande
-
Forced interactiviness to be true.
Preview Diff
1 | === modified file 'lava/config.py' |
2 | --- lava/config.py 2013-06-20 08:10:33 +0000 |
3 | +++ lava/config.py 2013-06-20 08:10:33 +0000 |
4 | @@ -51,7 +51,7 @@ |
5 | |
6 | class InteractiveConfig(object): |
7 | |
8 | - def __init__(self, force_interactive=False): |
9 | + def __init__(self, force_interactive=True): |
10 | self._force_interactive = force_interactive |
11 | self._cache = {} |
12 | |
13 | |
14 | === modified file 'lava/device/commands.py' |
15 | --- lava/device/commands.py 2013-06-20 08:10:33 +0000 |
16 | +++ lava/device/commands.py 2013-06-20 08:10:33 +0000 |
17 | @@ -21,23 +21,21 @@ |
18 | """ |
19 | |
20 | import os |
21 | -import subprocess |
22 | import sys |
23 | -import random |
24 | -import string |
25 | - |
26 | -from lava.config import InteractiveConfig |
27 | + |
28 | +from lava.helper.command import ( |
29 | + BaseCommand, |
30 | +) |
31 | + |
32 | +from lava.helper.dispatcher import ( |
33 | + get_device_file, |
34 | + get_devices_path, |
35 | +) |
36 | + |
37 | from lava.device import get_known_device |
38 | -from lava.tool.command import Command, CommandGroup |
39 | +from lava.tool.command import CommandGroup |
40 | from lava.tool.errors import CommandError |
41 | -from lava_tool.utils import has_command |
42 | |
43 | -# Default lava-dispatcher paths. |
44 | -DEFAULT_DISPATCHER_PATH = os.path.join("etc", "lava-dispatcher") |
45 | -USER_DISPATCHER_PATH = os.path.join(os.path.expanduser("~"), ".config", |
46 | - "lava-dispatcher") |
47 | -# Default devices path, has to be joined with the dispatcher path. |
48 | -DEFAULT_DEVICES_PATH = "devices" |
49 | DEVICE_FILE_SUFFIX = "conf" |
50 | |
51 | |
52 | @@ -47,119 +45,6 @@ |
53 | namespace = "lava.device.commands" |
54 | |
55 | |
56 | -class BaseCommand(Command): |
57 | - """Base command for device commands.""" |
58 | - def __init__(self, parser, args): |
59 | - super(BaseCommand, self).__init__(parser, args) |
60 | - self.config = InteractiveConfig( |
61 | - force_interactive=self.args.interactive) |
62 | - |
63 | - @classmethod |
64 | - def register_arguments(cls, parser): |
65 | - super(BaseCommand, cls).register_arguments(parser) |
66 | - parser.add_argument("-i", "--interactive", |
67 | - action='store_true', |
68 | - help=("Forces asking for input parameters even if " |
69 | - "we already have them cached.")) |
70 | - |
71 | - @classmethod |
72 | - def _get_dispatcher_paths(cls): |
73 | - """Tries to get the dispatcher from lava-dispatcher.""" |
74 | - try: |
75 | - from lava_dispatcher.config import write_path |
76 | - return write_path() |
77 | - except ImportError: |
78 | - raise CommandError("Cannot find lava-dispatcher installation.") |
79 | - |
80 | - @classmethod |
81 | - def _choose_devices_path(cls, paths): |
82 | - """Picks the first path that is writable by the user. |
83 | - |
84 | - :param paths: A list of paths. |
85 | - :return The first path where it is possible to write. |
86 | - """ |
87 | - valid_path = None |
88 | - for path in paths: |
89 | - path = os.path.join(path, DEFAULT_DEVICES_PATH) |
90 | - if os.path.exists(path): |
91 | - name = "".join(random.choice(string.ascii_letters) |
92 | - for x in range(6)) |
93 | - test_file = os.path.join(path, name) |
94 | - try: |
95 | - fp = open(test_file, 'a') |
96 | - fp.close() |
97 | - except IOError: |
98 | - # Cannot write here. |
99 | - continue |
100 | - else: |
101 | - valid_path = path |
102 | - if os.path.isfile(test_file): |
103 | - os.unlink(test_file) |
104 | - break |
105 | - else: |
106 | - try: |
107 | - os.makedirs(path) |
108 | - except OSError: |
109 | - # Cannot write here either. |
110 | - continue |
111 | - else: |
112 | - valid_path = path |
113 | - break |
114 | - else: |
115 | - raise CommandError("Insufficient permissions to create new " |
116 | - "devices.") |
117 | - return valid_path |
118 | - |
119 | - @classmethod |
120 | - def _get_devices_path(cls): |
121 | - """Gets the path to the devices in the LAVA dispatcher.""" |
122 | - dispatcher_paths = cls._get_dispatcher_paths() |
123 | - return cls._choose_devices_path(dispatcher_paths) |
124 | - |
125 | - @classmethod |
126 | - def edit_config_file(cls, config_file): |
127 | - """Opens the specified file with the default file editor. |
128 | - |
129 | - :param config_file: The file to edit. |
130 | - """ |
131 | - editor = os.environ.get("EDITOR", None) |
132 | - if editor is None: |
133 | - if has_command("sensible-editor"): |
134 | - editor = "sensible-editor" |
135 | - elif has_command("xdg-open"): |
136 | - editor = "xdg-open" |
137 | - else: |
138 | - # We really do not know how to open a file. |
139 | - print >> sys.stdout, ("Cannot find an editor to open the " |
140 | - "file '{0}'.".format(config_file)) |
141 | - print >> sys.stdout, ("Either set the 'EDITOR' environment " |
142 | - "variable, or install 'sensible-editor' " |
143 | - "or 'xdg-open'.") |
144 | - sys.exit(-1) |
145 | - |
146 | - try: |
147 | - subprocess.Popen([editor, config_file]).wait() |
148 | - except Exception: |
149 | - raise CommandError("Error opening the file '{0}' with the " |
150 | - "following editor: {1}.".format(config_file, |
151 | - editor)) |
152 | - |
153 | - @classmethod |
154 | - def _get_device_file(cls, file_name): |
155 | - """Retrieves the config file name specified, if it exists. |
156 | - |
157 | - :param file_name: The config file name to search. |
158 | - :return The path to the file, or None if it does not exist. |
159 | - """ |
160 | - try: |
161 | - from lava_dispatcher.config import get_config_file |
162 | - |
163 | - return get_config_file(os.path.join(DEFAULT_DEVICES_PATH, |
164 | - file_name)) |
165 | - except ImportError: |
166 | - raise CommandError("Cannot find lava-dispatcher installation.") |
167 | - |
168 | - |
169 | class add(BaseCommand): |
170 | """Adds a new device.""" |
171 | |
172 | @@ -171,14 +56,14 @@ |
173 | def invoke(self): |
174 | real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX]) |
175 | |
176 | - if self._get_device_file(real_file_name): |
177 | + if get_device_file(real_file_name): |
178 | print >> sys.stdout, ("A device configuration file named '{0}' " |
179 | "already exists.".format(real_file_name)) |
180 | print >> sys.stdout, ("Use 'lava device config {0}' to edit " |
181 | "it.".format(self.args.DEVICE)) |
182 | sys.exit(-1) |
183 | |
184 | - devices_path = self._get_devices_path() |
185 | + devices_path = get_devices_path() |
186 | device_conf_file = os.path.abspath(os.path.join(devices_path, |
187 | real_file_name)) |
188 | |
189 | @@ -187,7 +72,7 @@ |
190 | |
191 | print >> sys.stdout, ("Created device file '{0}' in: {1}".format( |
192 | real_file_name, devices_path)) |
193 | - self.edit_config_file(device_conf_file) |
194 | + self.edit_file(device_conf_file) |
195 | |
196 | |
197 | class remove(BaseCommand): |
198 | @@ -201,7 +86,7 @@ |
199 | |
200 | def invoke(self): |
201 | real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX]) |
202 | - device_conf = self._get_device_file(real_file_name) |
203 | + device_conf = get_device_file(real_file_name) |
204 | |
205 | if device_conf: |
206 | try: |
207 | @@ -224,27 +109,12 @@ |
208 | parser.add_argument("DEVICE", |
209 | help="The name of the device to edit.") |
210 | |
211 | - @classmethod |
212 | - def can_edit_file(cls, conf_file): |
213 | - """Checks if a file can be opend in write mode. |
214 | - |
215 | - :param conf_file: The path to the file. |
216 | - :return True if it is possible to write on the file, False otherwise. |
217 | - """ |
218 | - can_edit = True |
219 | - try: |
220 | - fp = open(conf_file, "a") |
221 | - fp.close() |
222 | - except IOError: |
223 | - can_edit = False |
224 | - return can_edit |
225 | - |
226 | def invoke(self): |
227 | real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX]) |
228 | + device_conf = get_device_file(real_file_name) |
229 | |
230 | - device_conf = self._get_device_file(real_file_name) |
231 | if device_conf and self.can_edit_file(device_conf): |
232 | - self.edit_config_file(device_conf) |
233 | + self.edit_file(device_conf) |
234 | else: |
235 | raise CommandError("Cannot edit file '{0}' at: " |
236 | "{1}.".format(real_file_name, device_conf)) |
237 | |
238 | === modified file 'lava/device/tests/test_commands.py' |
239 | --- lava/device/tests/test_commands.py 2013-06-20 08:10:33 +0000 |
240 | +++ lava/device/tests/test_commands.py 2013-06-20 08:10:33 +0000 |
241 | @@ -17,159 +17,84 @@ |
242 | # along with lava-tool. If not, see <http://www.gnu.org/licenses/>. |
243 | |
244 | """ |
245 | -Commands class unit tests. |
246 | +lava.device.commands unit tests. |
247 | """ |
248 | |
249 | import os |
250 | -import shutil |
251 | -import sys |
252 | -import tempfile |
253 | |
254 | -from mock import MagicMock |
255 | -from unittest import TestCase |
256 | +from mock import MagicMock, patch |
257 | |
258 | from lava.device.commands import ( |
259 | - BaseCommand, |
260 | add, |
261 | config, |
262 | remove, |
263 | ) |
264 | - |
265 | +from lava.helper.tests.helper_test import HelperTest |
266 | from lava.tool.errors import CommandError |
267 | |
268 | |
269 | -class CommandsTest(TestCase): |
270 | - def setUp(self): |
271 | - # Fake the stdout. |
272 | - self.original_stdout = sys.stdout |
273 | - sys.stdout = open("/dev/null", "w") |
274 | - self.original_stderr = sys.stderr |
275 | - sys.stderr = open("/dev/null", "w") |
276 | - self.original_stdin = sys.stdin |
277 | - |
278 | - self.device = "a_fake_panda02" |
279 | - |
280 | - self.tempdir = tempfile.mkdtemp() |
281 | - self.parser = MagicMock() |
282 | - self.args = MagicMock() |
283 | - self.args.interactive = MagicMock(return_value=False) |
284 | - self.args.DEVICE = self.device |
285 | - |
286 | - self.config = MagicMock() |
287 | - self.config.get = MagicMock(return_value=self.tempdir) |
288 | - |
289 | - def tearDown(self): |
290 | - sys.stdin = self.original_stdin |
291 | - sys.stdout = self.original_stdout |
292 | - sys.stderr = self.original_stderr |
293 | - shutil.rmtree(self.tempdir) |
294 | - |
295 | - def test_get_devices_path_0(self): |
296 | - # Tests that the correct devices path is returned and created. |
297 | - base_command = BaseCommand(self.parser, self.args) |
298 | - base_command.config = self.config |
299 | - BaseCommand._get_dispatcher_paths = MagicMock(return_value=[ |
300 | - self.tempdir]) |
301 | - obtained = base_command._get_devices_path() |
302 | - expected_path = os.path.join(self.tempdir, "devices") |
303 | - self.assertEqual(expected_path, obtained) |
304 | - self.assertTrue(os.path.isdir(expected_path)) |
305 | - |
306 | - def test_get_devices_path_1(self): |
307 | - # Tests that when passing a path that is not writable, CommandError |
308 | - # is raised. |
309 | - base_command = BaseCommand(self.parser, self.args) |
310 | - base_command.config = self.config |
311 | - BaseCommand._get_dispatcher_paths = MagicMock( |
312 | - return_value=["/", "/root", "/root/tmpdir"]) |
313 | - self.assertRaises(CommandError, base_command._get_devices_path) |
314 | - |
315 | - def test_choose_devices_path(self): |
316 | - # Tests that when passing more than one path, the first writable one |
317 | - # is returned. |
318 | - base_command = BaseCommand(self.parser, self.args) |
319 | - base_command.config = self.config |
320 | - obtained = base_command._choose_devices_path( |
321 | - ["/", "/root", self.tempdir, os.path.expanduser("~")]) |
322 | - expected = os.path.join(self.tempdir, "devices") |
323 | - self.assertEqual(expected, obtained) |
324 | - |
325 | - def test_add_invoke(self): |
326 | +class CommandsTest(HelperTest): |
327 | + |
328 | + @patch("lava.device.commands.get_device_file", |
329 | + new=MagicMock(return_value=None)) |
330 | + @patch("lava.device.commands.get_devices_path") |
331 | + def test_add_invoke(self, get_devices_path_mock): |
332 | # Tests invocation of the add command. Verifies that the conf file is |
333 | # written to disk. |
334 | + get_devices_path_mock.return_value = self.temp_dir |
335 | + |
336 | add_command = add(self.parser, self.args) |
337 | - add_command.edit_config_file = MagicMock() |
338 | - add_command._get_device_file = MagicMock(return_value=None) |
339 | - add_command._get_devices_path = MagicMock(return_value=self.tempdir) |
340 | + add_command.edit_file = MagicMock() |
341 | add_command.invoke() |
342 | |
343 | - expected_path = os.path.join(self.tempdir, |
344 | + expected_path = os.path.join(self.temp_dir, |
345 | ".".join([self.device, "conf"])) |
346 | self.assertTrue(os.path.isfile(expected_path)) |
347 | |
348 | - def test_remove_invoke(self): |
349 | + @patch("lava.device.commands.get_device_file") |
350 | + @patch("lava.device.commands.get_devices_path") |
351 | + def test_remove_invoke(self, get_devices_path_mock, get_device_file_mock): |
352 | # Tests invocation of the remove command. Verifies that the conf file |
353 | # has been correctly removed. |
354 | # First we add a new conf file, then we remove it. |
355 | + get_device_file_mock.return_value = None |
356 | + get_devices_path_mock.return_value = self.temp_dir |
357 | + |
358 | add_command = add(self.parser, self.args) |
359 | - add_command.edit_config_file = MagicMock() |
360 | - add_command._get_device_file = MagicMock(return_value=None) |
361 | - add_command._get_devices_path = MagicMock(return_value=self.tempdir) |
362 | + add_command.edit_file = MagicMock() |
363 | add_command.invoke() |
364 | |
365 | - expected_path = os.path.join(self.tempdir, |
366 | + expected_path = os.path.join(self.temp_dir, |
367 | ".".join([self.device, "conf"])) |
368 | |
369 | + # Set new values for the mocked function. |
370 | + get_device_file_mock.return_value = expected_path |
371 | + |
372 | remove_command = remove(self.parser, self.args) |
373 | - remove_command._get_device_file = MagicMock(return_value=expected_path) |
374 | - remove_command._get_devices_path = MagicMock(return_value=self.tempdir) |
375 | remove_command.invoke() |
376 | |
377 | self.assertFalse(os.path.isfile(expected_path)) |
378 | |
379 | + @patch("lava.device.commands.get_device_file", |
380 | + new=MagicMock(return_value="/root")) |
381 | def test_remove_invoke_raises(self): |
382 | # Tests invocation of the remove command, with a non existent device |
383 | # configuration file. |
384 | remove_command = remove(self.parser, self.args) |
385 | - remove_command._get_device_file = MagicMock(return_value="/root") |
386 | - |
387 | self.assertRaises(CommandError, remove_command.invoke) |
388 | |
389 | + @patch("lava.device.commands.get_device_file", |
390 | + new=MagicMock(return_value=None)) |
391 | def test_config_invoke_raises_0(self): |
392 | # Tests invocation of the config command, with a non existent device |
393 | # configuration file. |
394 | config_command = config(self.parser, self.args) |
395 | - config_command._get_device_file = MagicMock(return_value=None) |
396 | - |
397 | self.assertRaises(CommandError, config_command.invoke) |
398 | |
399 | + @patch("lava.device.commands.get_device_file", |
400 | + new=MagicMock(return_value="/etc/password")) |
401 | def test_config_invoke_raises_1(self): |
402 | # Tests invocation of the config command, with a non writable file. |
403 | # Hopefully tests are not run as root. |
404 | config_command = config(self.parser, self.args) |
405 | - config_command._get_device_file = MagicMock(return_value="/etc/passwd") |
406 | - |
407 | self.assertRaises(CommandError, config_command.invoke) |
408 | - |
409 | - def test_can_edit_file(self): |
410 | - # Tests the can_edit_file method of the config command. |
411 | - # This is to make sure the device config file is not erased when |
412 | - # checking if it is possible to open it. |
413 | - expected = ("hostname = a_fake_panda02\nconnection_command = \n" |
414 | - "device_type = panda\n") |
415 | - |
416 | - config_command = config(self.parser, self.args) |
417 | - try: |
418 | - conf_file = tempfile.NamedTemporaryFile(delete=False) |
419 | - |
420 | - with open(conf_file.name, "w") as f: |
421 | - f.write(expected) |
422 | - |
423 | - self.assertTrue(config_command.can_edit_file(conf_file.name)) |
424 | - obtained = "" |
425 | - with open(conf_file.name) as f: |
426 | - obtained = f.read() |
427 | - |
428 | - self.assertEqual(expected, obtained) |
429 | - finally: |
430 | - os.unlink(conf_file.name) |
431 | |
432 | === modified file 'lava/device/tests/test_device.py' |
433 | --- lava/device/tests/test_device.py 2013-06-20 08:10:33 +0000 |
434 | +++ lava/device/tests/test_device.py 2013-06-20 08:10:33 +0000 |
435 | @@ -20,34 +20,19 @@ |
436 | Device class unit tests. |
437 | """ |
438 | |
439 | -import os |
440 | import sys |
441 | -import tempfile |
442 | |
443 | from StringIO import StringIO |
444 | -from unittest import TestCase |
445 | |
446 | from lava.device import ( |
447 | Device, |
448 | get_known_device, |
449 | ) |
450 | from lava.tool.errors import CommandError |
451 | - |
452 | - |
453 | -class DeviceTest(TestCase): |
454 | - |
455 | - def setUp(self): |
456 | - # Fake the stdin and the stdout. |
457 | - self.original_stdout = sys.stdout |
458 | - sys.stdout = open("/dev/null", "w") |
459 | - self.original_stdin = sys.stdin |
460 | - sys.stdin = StringIO() |
461 | - self.temp_file = tempfile.NamedTemporaryFile(delete=False) |
462 | - |
463 | - def tearDown(self): |
464 | - sys.stdout = self.original_stdout |
465 | - sys.stdin = self.original_stdin |
466 | - os.unlink(self.temp_file.name) |
467 | +from lava.helper.tests.helper_test import HelperTest |
468 | + |
469 | + |
470 | +class DeviceTest(HelperTest): |
471 | |
472 | def test_get_known_device_panda_0(self): |
473 | # User creates a new device with a guessable name for a device. |
474 | @@ -108,6 +93,7 @@ |
475 | def test_get_known_device_raises(self): |
476 | # User tries to create a new device, but in some way nothing is passed |
477 | # on the command line when asked. |
478 | + sys.stdin = StringIO("\n") |
479 | self.assertRaises(CommandError, get_known_device, 'a_fake_device') |
480 | |
481 | def test_device_write(self): |
482 | |
483 | === added directory 'lava/helper' |
484 | === added file 'lava/helper/__init__.py' |
485 | === added file 'lava/helper/command.py' |
486 | --- lava/helper/command.py 1970-01-01 00:00:00 +0000 |
487 | +++ lava/helper/command.py 2013-06-20 08:10:33 +0000 |
488 | @@ -0,0 +1,101 @@ |
489 | +# Copyright (C) 2013 Linaro Limited |
490 | +# |
491 | +# Author: Milo Casagrande <milo.casagrande@linaro.org> |
492 | +# |
493 | +# This file is part of lava-tool. |
494 | +# |
495 | +# lava-tool is free software: you can redistribute it and/or modify |
496 | +# it under the terms of the GNU Lesser General Public License version 3 |
497 | +# as published by the Free Software Foundation |
498 | +# |
499 | +# lava-tool is distributed in the hope that it will be useful, |
500 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
501 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
502 | +# GNU General Public License for more details. |
503 | +# |
504 | +# You should have received a copy of the GNU Lesser General Public License |
505 | +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. |
506 | + |
507 | +"""Base command class common to lava commands series.""" |
508 | + |
509 | +import os |
510 | +import subprocess |
511 | +import sys |
512 | + |
513 | + |
514 | +from lava.config import InteractiveConfig |
515 | +from lava.tool.command import Command |
516 | +from lava.tool.errors import CommandError |
517 | +from lava_tool.utils import has_command |
518 | + |
519 | + |
520 | +class BaseCommand(Command): |
521 | + """Base command class for all lava commands.""" |
522 | + def __init__(self, parser, args): |
523 | + super(BaseCommand, self).__init__(parser, args) |
524 | + self.config = InteractiveConfig( |
525 | + force_interactive=self.args.non_interactive) |
526 | + |
527 | + @classmethod |
528 | + def register_arguments(cls, parser): |
529 | + super(BaseCommand, cls).register_arguments(parser) |
530 | + parser.add_argument("--non-interactive", |
531 | + action='store_false', |
532 | + help=("Do not ask for input parameters.")) |
533 | + |
534 | + @classmethod |
535 | + def can_edit_file(cls, conf_file): |
536 | + """Checks if a file can be opend in write mode. |
537 | + |
538 | + :param conf_file: The path to the file. |
539 | + :return True if it is possible to write on the file, False otherwise. |
540 | + """ |
541 | + can_edit = True |
542 | + try: |
543 | + fp = open(conf_file, "a") |
544 | + fp.close() |
545 | + except IOError: |
546 | + can_edit = False |
547 | + return can_edit |
548 | + |
549 | + @classmethod |
550 | + def edit_file(cls, config_file): |
551 | + """Opens the specified file with the default file editor. |
552 | + |
553 | + :param config_file: The file to edit. |
554 | + """ |
555 | + editor = os.environ.get("EDITOR", None) |
556 | + if editor is None: |
557 | + if has_command("sensible-editor"): |
558 | + editor = "sensible-editor" |
559 | + elif has_command("xdg-open"): |
560 | + editor = "xdg-open" |
561 | + else: |
562 | + # We really do not know how to open a file. |
563 | + print >> sys.stdout, ("Cannot find an editor to open the " |
564 | + "file '{0}'.".format(config_file)) |
565 | + print >> sys.stdout, ("Either set the 'EDITOR' environment " |
566 | + "variable, or install 'sensible-editor' " |
567 | + "or 'xdg-open'.") |
568 | + sys.exit(-1) |
569 | + try: |
570 | + subprocess.Popen([editor, config_file]).wait() |
571 | + except Exception: |
572 | + raise CommandError("Error opening the file '{0}' with the " |
573 | + "following editor: {1}.".format(config_file, |
574 | + editor)) |
575 | + |
576 | + @classmethod |
577 | + def run(cls, cmd_args): |
578 | + """Runs the supplied command args. |
579 | + |
580 | + :param cmd_args: The command, and its optional arguments, to run. |
581 | + :return The command execution return code. |
582 | + """ |
583 | + if not isinstance(cmd_args, list): |
584 | + cmd_args = list(cmd_args) |
585 | + try: |
586 | + return subprocess.check_call(cmd_args) |
587 | + except subprocess.CalledProcessError: |
588 | + raise CommandError("Error running the following command: " |
589 | + "{0}".format(" ".join(cmd_args))) |
590 | |
591 | === added file 'lava/helper/dispatcher.py' |
592 | --- lava/helper/dispatcher.py 1970-01-01 00:00:00 +0000 |
593 | +++ lava/helper/dispatcher.py 2013-06-20 08:10:33 +0000 |
594 | @@ -0,0 +1,110 @@ |
595 | +# Copyright (C) 2013 Linaro Limited |
596 | +# |
597 | +# Author: Milo Casagrande <milo.casagrande@linaro.org> |
598 | +# |
599 | +# This file is part of lava-tool. |
600 | +# |
601 | +# lava-tool is free software: you can redistribute it and/or modify |
602 | +# it under the terms of the GNU Lesser General Public License version 3 |
603 | +# as published by the Free Software Foundation |
604 | +# |
605 | +# lava-tool is distributed in the hope that it will be useful, |
606 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
607 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
608 | +# GNU General Public License for more details. |
609 | +# |
610 | +# You should have received a copy of the GNU Lesser General Public License |
611 | +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. |
612 | + |
613 | +"""Classes and functions to interact with the lava-dispatcher.""" |
614 | + |
615 | +import random |
616 | +import string |
617 | +import os |
618 | + |
619 | +from lava.tool.errors import CommandError |
620 | + |
621 | +# Default devices path, has to be joined with the dispatcher path. |
622 | +DEFAULT_DEVICES_PATH = "devices" |
623 | + |
624 | + |
625 | +def get_dispatcher_paths(): |
626 | + """Tries to get the dispatcher paths from lava-dispatcher. |
627 | + |
628 | + :return A list of paths. |
629 | + """ |
630 | + try: |
631 | + from lava_dispatcher.config import write_path |
632 | + return write_path() |
633 | + except ImportError: |
634 | + raise CommandError("Cannot find lava-dispatcher installation.") |
635 | + |
636 | + |
637 | +def get_devices(): |
638 | + """Gets the devices list from the dispatcher. |
639 | + |
640 | + :return A list of DeviceConfig. |
641 | + """ |
642 | + try: |
643 | + from lava_dispatcher.config import get_devices |
644 | + return get_devices() |
645 | + except ImportError: |
646 | + raise CommandError("Cannot find lava-dispatcher installation.") |
647 | + |
648 | + |
649 | +def get_device_file(file_name): |
650 | + """Retrieves the config file name specified, if it exists. |
651 | + |
652 | + :param file_name: The config file name to search. |
653 | + :return The path to the file, or None if it does not exist. |
654 | + """ |
655 | + try: |
656 | + from lava_dispatcher.config import get_config_file |
657 | + return get_config_file(os.path.join(DEFAULT_DEVICES_PATH, |
658 | + file_name)) |
659 | + except ImportError: |
660 | + raise CommandError("Cannot find lava-dispatcher installation.") |
661 | + |
662 | + |
663 | +def choose_devices_path(paths): |
664 | + """Picks the first path that is writable by the user. |
665 | + |
666 | + :param paths: A list of paths. |
667 | + :return The first path where it is possible to write. |
668 | + """ |
669 | + valid_path = None |
670 | + for path in paths: |
671 | + path = os.path.join(path, DEFAULT_DEVICES_PATH) |
672 | + if os.path.exists(path): |
673 | + name = "".join(random.choice(string.ascii_letters) |
674 | + for x in range(6)) |
675 | + test_file = os.path.join(path, name) |
676 | + try: |
677 | + fp = open(test_file, 'a') |
678 | + fp.close() |
679 | + except IOError: |
680 | + # Cannot write here. |
681 | + continue |
682 | + else: |
683 | + valid_path = path |
684 | + if os.path.isfile(test_file): |
685 | + os.unlink(test_file) |
686 | + break |
687 | + else: |
688 | + try: |
689 | + os.makedirs(path) |
690 | + except OSError: |
691 | + # Cannot write here either. |
692 | + continue |
693 | + else: |
694 | + valid_path = path |
695 | + break |
696 | + else: |
697 | + raise CommandError("Insufficient permissions to create new " |
698 | + "devices.") |
699 | + return valid_path |
700 | + |
701 | + |
702 | +def get_devices_path(): |
703 | + """Gets the path to the devices in the LAVA dispatcher.""" |
704 | + return choose_devices_path(get_dispatcher_paths()) |
705 | |
706 | === added directory 'lava/helper/tests' |
707 | === added file 'lava/helper/tests/__init__.py' |
708 | === added file 'lava/helper/tests/helper_test.py' |
709 | --- lava/helper/tests/helper_test.py 1970-01-01 00:00:00 +0000 |
710 | +++ lava/helper/tests/helper_test.py 2013-06-20 08:10:33 +0000 |
711 | @@ -0,0 +1,61 @@ |
712 | +# Copyright (C) 2013 Linaro Limited |
713 | +# |
714 | +# Author: Milo Casagrande <milo.casagrande@linaro.org> |
715 | +# |
716 | +# This file is part of lava-tool. |
717 | +# |
718 | +# lava-tool is free software: you can redistribute it and/or modify |
719 | +# it under the terms of the GNU Lesser General Public License version 3 |
720 | +# as published by the Free Software Foundation |
721 | +# |
722 | +# lava-tool is distributed in the hope that it will be useful, |
723 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
724 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
725 | +# GNU General Public License for more details. |
726 | +# |
727 | +# You should have received a copy of the GNU Lesser General Public License |
728 | +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. |
729 | + |
730 | +""" |
731 | +A test helper class. |
732 | + |
733 | +Here we define a general test class and its own setUp and tearDown methods that |
734 | +all other test classes can inherit from. |
735 | +""" |
736 | + |
737 | +import os |
738 | +import shutil |
739 | +import sys |
740 | +import tempfile |
741 | + |
742 | +from unittest import TestCase |
743 | +from mock import MagicMock |
744 | + |
745 | + |
746 | +class HelperTest(TestCase): |
747 | + """Helper test class that all tests under the lava package can inherit.""" |
748 | + def setUp(self): |
749 | + self.original_stdout = sys.stdout |
750 | + sys.stdout = open("/dev/null", "w") |
751 | + self.original_stderr = sys.stderr |
752 | + sys.stderr = open("/dev/null", "w") |
753 | + self.original_stdin = sys.stdin |
754 | + |
755 | + self.device = "a_fake_panda02" |
756 | + |
757 | + self.temp_file = tempfile.NamedTemporaryFile(delete=False) |
758 | + self.temp_dir = tempfile.mkdtemp() |
759 | + self.parser = MagicMock() |
760 | + self.args = MagicMock() |
761 | + self.args.interactive = MagicMock(return_value=False) |
762 | + self.args.DEVICE = self.device |
763 | + |
764 | + self.config = MagicMock() |
765 | + self.config.get = MagicMock(return_value=self.temp_dir) |
766 | + |
767 | + def tearDown(self): |
768 | + sys.stdin = self.original_stdin |
769 | + sys.stdout = self.original_stdout |
770 | + sys.stderr = self.original_stderr |
771 | + shutil.rmtree(self.temp_dir) |
772 | + os.unlink(self.temp_file.name) |
773 | |
774 | === added file 'lava/helper/tests/test_command.py' |
775 | --- lava/helper/tests/test_command.py 1970-01-01 00:00:00 +0000 |
776 | +++ lava/helper/tests/test_command.py 2013-06-20 08:10:33 +0000 |
777 | @@ -0,0 +1,53 @@ |
778 | +# Copyright (C) 2013 Linaro Limited |
779 | +# |
780 | +# Author: Milo Casagrande <milo.casagrande@linaro.org> |
781 | +# |
782 | +# This file is part of lava-tool. |
783 | +# |
784 | +# lava-tool is free software: you can redistribute it and/or modify |
785 | +# it under the terms of the GNU Lesser General Public License version 3 |
786 | +# as published by the Free Software Foundation |
787 | +# |
788 | +# lava-tool is distributed in the hope that it will be useful, |
789 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
790 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
791 | +# GNU General Public License for more details. |
792 | +# |
793 | +# You should have received a copy of the GNU Lesser General Public License |
794 | +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. |
795 | + |
796 | +"""lava.herlp.command module tests.""" |
797 | + |
798 | +from lava.helper.command import BaseCommand |
799 | +from lava.helper.tests.helper_test import HelperTest |
800 | + |
801 | + |
802 | +class BaseCommandTests(HelperTest): |
803 | + |
804 | + def test_register_argument(self): |
805 | + # Make sure that the parser add_argument is called and we have the |
806 | + # correct argument. |
807 | + command = BaseCommand(self.parser, self.args) |
808 | + command.register_arguments(self.parser) |
809 | + name, args, kwargs = self.parser.method_calls[0] |
810 | + self.assertIn("--non-interactive", args) |
811 | + |
812 | + def test_can_edit_file(self): |
813 | + # Tests the can_edit_file method of the config command. |
814 | + # This is to make sure the device config file is not erased when |
815 | + # checking if it is possible to open it. |
816 | + expected = ("hostname = a_fake_panda02\nconnection_command = \n" |
817 | + "device_type = panda\n") |
818 | + |
819 | + command = BaseCommand(self.parser, self.args) |
820 | + conf_file = self.temp_file |
821 | + |
822 | + with open(conf_file.name, "w") as f: |
823 | + f.write(expected) |
824 | + |
825 | + self.assertTrue(command.can_edit_file(conf_file.name)) |
826 | + obtained = "" |
827 | + with open(conf_file.name) as f: |
828 | + obtained = f.read() |
829 | + |
830 | + self.assertEqual(expected, obtained) |
831 | |
832 | === added file 'lava/helper/tests/test_dispatcher.py' |
833 | --- lava/helper/tests/test_dispatcher.py 1970-01-01 00:00:00 +0000 |
834 | +++ lava/helper/tests/test_dispatcher.py 2013-06-20 08:10:33 +0000 |
835 | @@ -0,0 +1,52 @@ |
836 | +# Copyright (C) 2013 Linaro Limited |
837 | +# |
838 | +# Author: Milo Casagrande <milo.casagrande@linaro.org> |
839 | +# |
840 | +# This file is part of lava-tool. |
841 | +# |
842 | +# lava-tool is free software: you can redistribute it and/or modify |
843 | +# it under the terms of the GNU Lesser General Public License version 3 |
844 | +# as published by the Free Software Foundation |
845 | +# |
846 | +# lava-tool is distributed in the hope that it will be useful, |
847 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
848 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
849 | +# GNU General Public License for more details. |
850 | +# |
851 | +# You should have received a copy of the GNU Lesser General Public License |
852 | +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. |
853 | + |
854 | +"""lava.helper.dispatcher tests.""" |
855 | + |
856 | +import os |
857 | +from lava.tool.errors import CommandError |
858 | + |
859 | +from lava.helper.tests.helper_test import HelperTest |
860 | + |
861 | +from lava.helper.dispatcher import ( |
862 | + choose_devices_path, |
863 | +) |
864 | + |
865 | + |
866 | +class DispatcherTests(HelperTest): |
867 | + |
868 | + def test_choose_devices_path_0(self): |
869 | + # Tests that when passing more than one path, the first writable one |
870 | + # is returned. |
871 | + obtained = choose_devices_path( |
872 | + ["/", "/root", self.temp_dir, os.path.expanduser("~")]) |
873 | + expected = os.path.join(self.temp_dir, "devices") |
874 | + self.assertEqual(expected, obtained) |
875 | + |
876 | + def test_choose_devices_path_1(self): |
877 | + # Tests that when passing a path that is not writable, CommandError |
878 | + # is raised. |
879 | + self.assertRaises(CommandError, choose_devices_path, |
880 | + ["/", "/root", "/root/tmpdir"]) |
881 | + |
882 | + def test_choose_devices_path_2(self): |
883 | + # Tests that the correct path for devices is created on the filesystem. |
884 | + expected_path = os.path.join(self.temp_dir, "devices") |
885 | + obtained = choose_devices_path([self.temp_dir]) |
886 | + self.assertEqual(expected_path, obtained) |
887 | + self.assertTrue(os.path.isdir(expected_path)) |
888 | |
889 | === modified file 'lava/job/__init__.py' |
890 | --- lava/job/__init__.py 2013-05-28 22:08:12 +0000 |
891 | +++ lava/job/__init__.py 2013-06-20 08:10:33 +0000 |
892 | @@ -21,12 +21,13 @@ |
893 | |
894 | from lava.job.templates import Parameter |
895 | |
896 | + |
897 | class Job: |
898 | - |
899 | def __init__(self, template): |
900 | self.data = deepcopy(template) |
901 | |
902 | def fill_in(self, config): |
903 | + |
904 | def insert_data(data): |
905 | if isinstance(data, dict): |
906 | keys = data.keys() |
907 | @@ -44,4 +45,3 @@ |
908 | |
909 | def write(self, stream): |
910 | stream.write(json.dumps(self.data, indent=4)) |
911 | - |
912 | |
913 | === modified file 'lava/job/commands.py' |
914 | --- lava/job/commands.py 2013-06-03 18:06:49 +0000 |
915 | +++ lava/job/commands.py 2013-06-20 08:10:33 +0000 |
916 | @@ -16,40 +16,34 @@ |
917 | # You should have received a copy of the GNU Lesser General Public License |
918 | # along with lava-tool. If not, see <http://www.gnu.org/licenses/>. |
919 | |
920 | -from os.path import exists |
921 | - |
922 | -from lava.config import InteractiveConfig |
923 | +""" |
924 | +LAVA job commands. |
925 | +""" |
926 | + |
927 | +import os |
928 | +import sys |
929 | +import xmlrpclib |
930 | + |
931 | +from lava.helper.command import BaseCommand |
932 | + |
933 | +from lava.config import Parameter |
934 | from lava.job import Job |
935 | -from lava.job.templates import * |
936 | -from lava.tool.command import Command, CommandGroup |
937 | +from lava.job.templates import ( |
938 | + BOOT_TEST, |
939 | +) |
940 | +from lava.tool.command import CommandGroup |
941 | from lava.tool.errors import CommandError |
942 | - |
943 | from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend |
944 | -import xmlrpclib |
945 | +from lava_tool.utils import has_command |
946 | + |
947 | |
948 | class job(CommandGroup): |
949 | - """ |
950 | - LAVA job file handling |
951 | - """ |
952 | - |
953 | + """LAVA job file handling.""" |
954 | namespace = 'lava.job.commands' |
955 | |
956 | -class BaseCommand(Command): |
957 | - |
958 | - def __init__(self, parser, args): |
959 | - super(BaseCommand, self).__init__(parser, args) |
960 | - self.config = InteractiveConfig(force_interactive=self.args.interactive) |
961 | - |
962 | - @classmethod |
963 | - def register_arguments(cls, parser): |
964 | - super(BaseCommand, cls).register_arguments(parser) |
965 | - parser.add_argument( |
966 | - "-i", "--interactive", |
967 | - action='store_true', |
968 | - help=("Forces asking for input parameters even if we already " |
969 | - "have them cached.")) |
970 | |
971 | class new(BaseCommand): |
972 | + """Creates a new job file.""" |
973 | |
974 | @classmethod |
975 | def register_arguments(cls, parser): |
976 | @@ -57,20 +51,22 @@ |
977 | parser.add_argument("FILE", help=("Job file to be created.")) |
978 | |
979 | def invoke(self): |
980 | - if exists(self.args.FILE): |
981 | - raise CommandError('%s already exists' % self.args.FILE) |
982 | + if os.path.exists(self.args.FILE): |
983 | + raise CommandError('{0} already exists.'.format(self.args.FILE)) |
984 | |
985 | - with open(self.args.FILE, 'w') as f: |
986 | - job = Job(BOOT_TEST) |
987 | - job.fill_in(self.config) |
988 | - job.write(f) |
989 | + with open(self.args.FILE, 'w') as job_file: |
990 | + job_instance = Job(BOOT_TEST) |
991 | + job_instance.fill_in(self.config) |
992 | + job_instance.write(job_file) |
993 | |
994 | |
995 | class submit(BaseCommand): |
996 | + """Submits the specified job file.""" |
997 | + |
998 | @classmethod |
999 | def register_arguments(cls, parser): |
1000 | super(submit, cls).register_arguments(parser) |
1001 | - parser.add_argument("FILE", help=("The job file to submit")) |
1002 | + parser.add_argument("FILE", help=("The job file to submit.")) |
1003 | |
1004 | def invoke(self): |
1005 | jobfile = self.args.FILE |
1006 | @@ -85,10 +81,61 @@ |
1007 | auth_backend=KeyringAuthBackend()) |
1008 | try: |
1009 | job_id = server.scheduler.submit_job(jobdata) |
1010 | - print "Job submitted with job ID %d" % job_id |
1011 | - except xmlrpclib.Fault, e: |
1012 | - raise CommandError(str(e)) |
1013 | + print >> sys.stdout, "Job submitted with job ID {0}".format(job_id) |
1014 | + except xmlrpclib.Fault, exc: |
1015 | + raise CommandError(str(exc)) |
1016 | + |
1017 | |
1018 | class run(BaseCommand): |
1019 | + """Runs the specified job file on the local dispatcher.""" |
1020 | + |
1021 | + @classmethod |
1022 | + def register_arguments(cls, parser): |
1023 | + super(run, cls).register_arguments(parser) |
1024 | + parser.add_argument("FILE", help=("The job file to submit.")) |
1025 | + |
1026 | + @classmethod |
1027 | + def _choose_device(cls, devices): |
1028 | + """Let the user choose the device to use. |
1029 | + |
1030 | + :param devices: The list of available devices. |
1031 | + :return The selected device. |
1032 | + """ |
1033 | + devices_len = len(devices) |
1034 | + output_list = [] |
1035 | + for device, number in zip(devices, range(1, devices_len + 1)): |
1036 | + output_list.append("\t{0}. {1}\n".format(number, device.hostname)) |
1037 | + |
1038 | + print >> sys.stdout, ("More than one local device found. " |
1039 | + "Please choose one:\n") |
1040 | + print >> sys.stdout, "".join(output_list) |
1041 | + |
1042 | + while True: |
1043 | + try: |
1044 | + user_input = raw_input("Device number to use: ").strip() |
1045 | + |
1046 | + if user_input in [str(x) for x in range(1, devices_len + 1)]: |
1047 | + return devices[int(user_input) - 1].hostname |
1048 | + else: |
1049 | + continue |
1050 | + except EOFError: |
1051 | + user_input = None |
1052 | + except KeyboardInterrupt: |
1053 | + sys.exit(-1) |
1054 | + |
1055 | def invoke(self): |
1056 | - print("hello world") |
1057 | + if os.path.isfile(self.args.FILE): |
1058 | + if has_command("lava-dispatch"): |
1059 | + devices = self.get_devices() |
1060 | + if devices: |
1061 | + if len(devices) > 1: |
1062 | + device = self._choose_device(devices) |
1063 | + else: |
1064 | + device = devices[0].hostname |
1065 | + self.run(["lava-dispatch", "--target", device, |
1066 | + self.args.FILE]) |
1067 | + else: |
1068 | + raise CommandError("Cannot find lava-dispatcher installation.") |
1069 | + else: |
1070 | + raise CommandError("The file '{0}' does not exists. or is not " |
1071 | + "a file.".format(self.args.FILE)) |
1072 | |
1073 | === modified file 'lava/job/tests/test_commands.py' |
1074 | --- lava/job/tests/test_commands.py 2013-06-03 18:06:49 +0000 |
1075 | +++ lava/job/tests/test_commands.py 2013-06-20 08:10:33 +0000 |
1076 | @@ -20,74 +20,95 @@ |
1077 | Unit tests for the commands classes |
1078 | """ |
1079 | |
1080 | -from argparse import ArgumentParser |
1081 | import json |
1082 | -from os import ( |
1083 | - makedirs, |
1084 | - removedirs, |
1085 | -) |
1086 | -from os.path import( |
1087 | - exists, |
1088 | - join, |
1089 | -) |
1090 | -from shutil import( |
1091 | - rmtree, |
1092 | -) |
1093 | -from tempfile import mkdtemp |
1094 | -from unittest import TestCase |
1095 | - |
1096 | -from lava.config import NonInteractiveConfig |
1097 | -from lava.job.commands import * |
1098 | +import os |
1099 | + |
1100 | +from mock import MagicMock, patch |
1101 | + |
1102 | +from lava.config import NonInteractiveConfig, Parameter |
1103 | + |
1104 | +from lava.job.commands import ( |
1105 | + new, |
1106 | + run, |
1107 | + submit, |
1108 | +) |
1109 | + |
1110 | +from lava.helper.tests.helper_test import HelperTest |
1111 | from lava.tool.errors import CommandError |
1112 | |
1113 | -from mocker import Mocker |
1114 | - |
1115 | -def make_command(command, *args): |
1116 | - parser = ArgumentParser(description="fake argument parser") |
1117 | - command.register_arguments(parser) |
1118 | - the_args = parser.parse_args(*args) |
1119 | - cmd = command(parser, the_args) |
1120 | - cmd.config = NonInteractiveConfig({ 'device_type': 'foo', 'prebuilt_image': 'bar' }) |
1121 | - return cmd |
1122 | - |
1123 | -class CommandTest(TestCase): |
1124 | + |
1125 | +class CommandTest(HelperTest): |
1126 | |
1127 | def setUp(self): |
1128 | - self.tmpdir = mkdtemp() |
1129 | + super(CommandTest, self).setUp() |
1130 | + self.args.FILE = self.temp_file.name |
1131 | |
1132 | - def tearDown(self): |
1133 | - rmtree(self.tmpdir) |
1134 | + self.device_type = Parameter('device_type') |
1135 | + self.prebuilt_image = Parameter('prebuilt_image', |
1136 | + depends=self.device_type) |
1137 | + self.config = NonInteractiveConfig( |
1138 | + {'device_type': 'foo', 'prebuilt_image': 'bar'}) |
1139 | |
1140 | def tmp(self, filename): |
1141 | - return join(self.tmpdir, filename) |
1142 | + """Returns a path to a non existent file. |
1143 | + |
1144 | + :param filename: The name the file should have. |
1145 | + :return A path. |
1146 | + """ |
1147 | + return os.path.join(self.temp_dir, filename) |
1148 | + |
1149 | |
1150 | class JobNewTest(CommandTest): |
1151 | |
1152 | + def setUp(self): |
1153 | + super(JobNewTest, self).setUp() |
1154 | + self.args.FILE = self.tmp("new_file.json") |
1155 | + self.new_command = new(self.parser, self.args) |
1156 | + self.new_command.config = self.config |
1157 | + |
1158 | + def tearDown(self): |
1159 | + super(JobNewTest, self).tearDown() |
1160 | + if os.path.exists(self.args.FILE): |
1161 | + os.unlink(self.args.FILE) |
1162 | + |
1163 | def test_create_new_file(self): |
1164 | - f = self.tmp('file.json') |
1165 | - command = make_command(new, [f]) |
1166 | - command.invoke() |
1167 | - self.assertTrue(exists(f)) |
1168 | + self.new_command.invoke() |
1169 | + self.assertTrue(os.path.exists(self.args.FILE)) |
1170 | |
1171 | def test_fills_in_template_parameters(self): |
1172 | - f = self.tmp('myjob.json') |
1173 | - command = make_command(new, [f]) |
1174 | - command.invoke() |
1175 | + self.new_command.invoke() |
1176 | |
1177 | - data = json.loads(open(f).read()) |
1178 | + data = json.loads(open(self.args.FILE).read()) |
1179 | self.assertEqual(data['device_type'], 'foo') |
1180 | |
1181 | - def test_wont_overwriteexisting_file(self): |
1182 | - existing = self.tmp('existing.json') |
1183 | - with open(existing, 'w') as f: |
1184 | + def test_wont_overwrite_existing_file(self): |
1185 | + with open(self.args.FILE, 'w') as f: |
1186 | f.write("CONTENTS") |
1187 | - command = make_command(new, [existing]) |
1188 | - with self.assertRaises(CommandError): |
1189 | - command.invoke() |
1190 | - self.assertEqual("CONTENTS", open(existing).read()) |
1191 | + |
1192 | + self.assertRaises(CommandError, self.new_command.invoke) |
1193 | + self.assertEqual("CONTENTS", open(self.args.FILE).read()) |
1194 | + |
1195 | |
1196 | class JobSubmitTest(CommandTest): |
1197 | |
1198 | def test_receives_job_file_in_cmdline(self): |
1199 | - cmd = make_command(new, ['FOO.json']) |
1200 | - self.assertEqual('FOO.json', cmd.args.FILE) |
1201 | + command = submit(self.parser, self.args) |
1202 | + command.register_arguments(self.parser) |
1203 | + name, args, kwargs = self.parser.method_calls[1] |
1204 | + self.assertIn("FILE", args) |
1205 | + |
1206 | + |
1207 | +class JobRunTest(CommandTest): |
1208 | + |
1209 | + def test_invoke_raises_0(self): |
1210 | + # Users passes a non existing job file to the run command. |
1211 | + self.args.FILE = self.tmp("test_invoke_raises_0.json") |
1212 | + command = run(self.parser, self.args) |
1213 | + self.assertRaises(CommandError, command.invoke) |
1214 | + |
1215 | + @patch("lava.job.commands.has_command", new=MagicMock(return_value=False)) |
1216 | + def test_invoke_raises_1(self): |
1217 | + # Users passes a valid file to the run command, but she does not have |
1218 | + # the dispatcher installed. |
1219 | + command = run(self.parser, self.args) |
1220 | + self.assertRaises(CommandError, command.invoke) |
1221 | |
1222 | === modified file 'lava_tool/tests/__init__.py' |
1223 | --- lava_tool/tests/__init__.py 2013-06-20 08:10:33 +0000 |
1224 | +++ lava_tool/tests/__init__.py 2013-06-20 08:10:33 +0000 |
1225 | @@ -38,11 +38,14 @@ |
1226 | 'lava_tool.tests.test_authtoken', |
1227 | 'lava_tool.tests.test_auth_commands', |
1228 | 'lava_tool.tests.test_commands', |
1229 | + 'lava_tool.tests.test_utils', |
1230 | 'lava_dashboard_tool.tests.test_commands', |
1231 | 'lava.job.tests.test_job', |
1232 | 'lava.job.tests.test_commands', |
1233 | 'lava.device.tests.test_device', |
1234 | 'lava.device.tests.test_commands', |
1235 | + 'lava.helper.tests.test_command', |
1236 | + 'lava.helper.tests.test_dispatcher', |
1237 | ] |
1238 | |
1239 | |
1240 | |
1241 | === added file 'lava_tool/tests/test_utils.py' |
1242 | --- lava_tool/tests/test_utils.py 1970-01-01 00:00:00 +0000 |
1243 | +++ lava_tool/tests/test_utils.py 2013-06-20 08:10:33 +0000 |
1244 | @@ -0,0 +1,41 @@ |
1245 | +# Copyright (C) 2013 Linaro Limited |
1246 | +# |
1247 | +# Author: Milo Casagrande <milo.casagrande@linaro.org> |
1248 | +# |
1249 | +# This file is part of lava-tool. |
1250 | +# |
1251 | +# lava-tool is free software: you can redistribute it and/or modify |
1252 | +# it under the terms of the GNU Lesser General Public License version 3 |
1253 | +# as published by the Free Software Foundation |
1254 | +# |
1255 | +# lava-tool is distributed in the hope that it will be useful, |
1256 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1257 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1258 | +# GNU General Public License for more details. |
1259 | +# |
1260 | +# You should have received a copy of the GNU Lesser General Public License |
1261 | +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. |
1262 | + |
1263 | +"""lava_tool.utils tests.""" |
1264 | + |
1265 | +import subprocess |
1266 | + |
1267 | +from unittest import TestCase |
1268 | +from mock import patch |
1269 | + |
1270 | +from lava_tool.utils import has_command |
1271 | + |
1272 | + |
1273 | +class UtilTests(TestCase): |
1274 | + |
1275 | + @patch("lava_tool.utils.subprocess.check_call") |
1276 | + def test_has_command_0(self, mocked_check_call): |
1277 | + # Make sure we raise an exception when the subprocess is called. |
1278 | + mocked_check_call.side_effect = subprocess.CalledProcessError(0, "") |
1279 | + self.assertFalse(has_command("")) |
1280 | + |
1281 | + @patch("lava_tool.utils.subprocess.check_call") |
1282 | + def test_has_command_1(self, mocked_check_call): |
1283 | + # Check that a "command" exists. The call to subprocess is mocked. |
1284 | + mocked_check_call.return_value = 0 |
1285 | + self.assertTrue(has_command("")) |
1286 | |
1287 | === modified file 'lava_tool/utils.py' |
1288 | --- lava_tool/utils.py 2013-06-20 08:10:33 +0000 |
1289 | +++ lava_tool/utils.py 2013-06-20 08:10:33 +0000 |
1290 | @@ -16,6 +16,7 @@ |
1291 | # You should have received a copy of the GNU Lesser General Public License |
1292 | # along with lava-tool. If not, see <http://www.gnu.org/licenses/>. |
1293 | |
1294 | +import os |
1295 | import subprocess |
1296 | |
1297 | |
1298 | @@ -27,7 +28,7 @@ |
1299 | command_available = True |
1300 | try: |
1301 | subprocess.check_call(["which", command], |
1302 | - stdout=open('/dev/null', 'w')) |
1303 | + stdout=open(os.path.devnull, 'w')) |
1304 | except subprocess.CalledProcessError: |
1305 | command_available = False |
1306 | return command_available |
Hi Milo,
Thanks for this. Some of my comments are related to the questions we already
talked about earlier today, but I will also mention them here so others can
also follow.
review needs-fixing
> === added file 'lava/base_ command. py' command. py 1970-01-01 00:00:00 +0000 command. py 2013-06-17 13:33:27 +0000
> --- lava/base_
> +++ lava/base_
I would put this somewhere like lava/helper/ command. py instead
Maybe when we start working on the other items (testdef/script) we realize that
we can move all of them in the lava.helper namespace instead of creating
lava.testdef and lava.script.
> @@ -0,0 +1,80 @@ www.gnu. org/licenses/>. Command) : _init__ (parser, args) ve=self. args.interactiv e) arguments( cls, parser): arguments( parser) add_argument( "-i", "--interactive", 'store_ true',
> +# Copyright (C) 2013 Linaro Limited
> +#
> +# Author: Milo Casagrande <email address hidden>
> +#
> +# This file is part of lava-tool.
> +#
> +# lava-tool is free software: you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License version 3
> +# as published by the Free Software Foundation
> +#
> +# lava-tool is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> +# GNU General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with lava-tool. If not, see <http://
> +
> +"""Base command class common to lava commands series."""
> +
> +import subprocess
> +
> +from lava.config import InteractiveConfig
> +from lava.tool.command import Command
> +from lava.tool.errors import CommandError
> +
> +
> +class BaseCommand(
> + """Base command class for all lava commands."""
> + def __init__(self, parser, args):
> + super(BaseCommand, self)._
> + self.config = InteractiveConfig(
> + force_interacti
> +
> + @classmethod
> + def register_
> + super(BaseCommand, cls).register_
> + parser.
> + action=
> + help=("Forces asking for input parameters even if "
> + "we already have them cached."))
As we talked ealier, I think it's better to do it the other way around: assume
interactivity always, and have a --non-interactive option to force not asking
for input.
> + paths(cls) : .config import write_path "Cannot find lava-dispatcher installation.") .config import get_devices
> + @classmethod
> + def get_dispatcher_
> + """Tries to get the dispatcher paths from lava-dispatcher.
> +
> + :return A list of paths.
> + """
> + try:
> + from lava_dispatcher
> + return write_path()
> + except ImportError:
> + raise CommandError(
> +
> + @classmethod
> + def get_devices(cls):
> + """Gets the devices list from the dispatcher.
> +
> + :return A list of DeviceConfig.
> + """
> + try:
> + from lava_dispatcher
> + return get_devices()
> + except ImportError:
> + raise Comman...