Merge lp:~milo/lava-tool/lava-168 into lp:~milo/lava-tool/lava-167

Proposed by Milo Casagrande
Status: Superseded
Proposed branch: lp:~milo/lava-tool/lava-168
Merge into: lp:~milo/lava-tool/lava-167
Diff against target: 429 lines (+247/-86)
4 files modified
lava/base_command.py (+80/-0)
lava/job/__init__.py (+2/-2)
lava/job/commands.py (+83/-36)
lava/job/tests/test_commands.py (+82/-48)
To merge this branch: bzr merge lp:~milo/lava-tool/lava-168
Reviewer Review Type Date Requested Status
Antonio Terceiro Pending
Linaro Automation & Validation Pending
Review via email: mp+169741@code.launchpad.net

Description of the change

This branch adds the 'lava job run' command to run a job file in the local dispatcher.

To post a comment you must log in.
lp:~milo/lava-tool/lava-168 updated
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.

207. By Milo Casagrande

PEP8 fixes.

208. By Milo Casagrande

Fixed problem with one test.

209. By Milo Casagrande

Forced interactiviness to be true.

Unmerged revisions

209. By Milo Casagrande

Forced interactiviness to be true.

208. By Milo Casagrande

Fixed problem with one test.

207. By Milo Casagrande

PEP8 fixes.

206. By Milo Casagrande

Fixed tests, use new test helper class.

205. By Milo Casagrande

Fixed command help.

204. By Milo Casagrande

Added dispatcher helper tests.

203. By Milo Casagrande

Added tests to the test suite.

202. By Milo Casagrande

Added test for the utils module.

201. By Milo Casagrande

Use os.path.devnull.

200. By Milo Casagrande

Use the new base command class.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lava/base_command.py'
2--- lava/base_command.py 1970-01-01 00:00:00 +0000
3+++ lava/base_command.py 2013-06-17 09:01:27 +0000
4@@ -0,0 +1,80 @@
5+# Copyright (C) 2013 Linaro Limited
6+#
7+# Author: Milo Casagrande <milo.casagrande@linaro.org>
8+#
9+# This file is part of lava-tool.
10+#
11+# lava-tool is free software: you can redistribute it and/or modify
12+# it under the terms of the GNU Lesser General Public License version 3
13+# as published by the Free Software Foundation
14+#
15+# lava-tool is distributed in the hope that it will be useful,
16+# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+# GNU General Public License for more details.
19+#
20+# You should have received a copy of the GNU Lesser General Public License
21+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
22+
23+"""Base command class common to lava commands series."""
24+
25+import subprocess
26+
27+from lava.config import InteractiveConfig
28+from lava.tool.command import Command
29+from lava.tool.errors import CommandError
30+
31+
32+class BaseCommand(Command):
33+ """Base command class for all lava commands."""
34+ def __init__(self, parser, args):
35+ super(BaseCommand, self).__init__(parser, args)
36+ self.config = InteractiveConfig(
37+ force_interactive=self.args.interactive)
38+
39+ @classmethod
40+ def register_arguments(cls, parser):
41+ super(BaseCommand, cls).register_arguments(parser)
42+ parser.add_argument("-i", "--interactive",
43+ action='store_true',
44+ help=("Forces asking for input parameters even if "
45+ "we already have them cached."))
46+
47+ @classmethod
48+ def get_dispatcher_paths(cls):
49+ """Tries to get the dispatcher paths from lava-dispatcher.
50+
51+ :return A list of paths.
52+ """
53+ try:
54+ from lava_dispatcher.config import write_path
55+ return write_path()
56+ except ImportError:
57+ raise CommandError("Cannot find lava-dispatcher installation.")
58+
59+ @classmethod
60+ def get_devices(cls):
61+ """Gets the devices list from the dispatcher.
62+
63+ :return A list of DeviceConfig.
64+ """
65+ try:
66+ from lava_dispatcher.config import get_devices
67+ return get_devices()
68+ except ImportError:
69+ raise CommandError("Cannot find lava-dispatcher installation.")
70+
71+ @classmethod
72+ def run(cls, cmd_args):
73+ """Runs the supplied command args.
74+
75+ :param cmd_args: The command, and its optional arguments, to run.
76+ :return The command execution return code.
77+ """
78+ if not isinstance(cmd_args, list):
79+ cmd_args = list(cmd_args)
80+ try:
81+ return subprocess.check_call(cmd_args)
82+ except subprocess.CalledProcessError:
83+ raise CommandError("Error running the following command: "
84+ "{0}".format(" ".join(cmd_args)))
85
86=== modified file 'lava/job/__init__.py'
87--- lava/job/__init__.py 2013-05-28 22:08:12 +0000
88+++ lava/job/__init__.py 2013-06-17 09:01:27 +0000
89@@ -21,12 +21,13 @@
90
91 from lava.job.templates import Parameter
92
93+
94 class Job:
95-
96 def __init__(self, template):
97 self.data = deepcopy(template)
98
99 def fill_in(self, config):
100+
101 def insert_data(data):
102 if isinstance(data, dict):
103 keys = data.keys()
104@@ -44,4 +45,3 @@
105
106 def write(self, stream):
107 stream.write(json.dumps(self.data, indent=4))
108-
109
110=== modified file 'lava/job/commands.py'
111--- lava/job/commands.py 2013-06-03 18:06:49 +0000
112+++ lava/job/commands.py 2013-06-17 09:01:27 +0000
113@@ -16,40 +16,34 @@
114 # You should have received a copy of the GNU Lesser General Public License
115 # along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
116
117-from os.path import exists
118-
119-from lava.config import InteractiveConfig
120+"""
121+LAVA job commands.
122+"""
123+
124+import os
125+import sys
126+import xmlrpclib
127+
128+from lava.base_command import BaseCommand
129+
130+from lava.config import Parameter
131 from lava.job import Job
132-from lava.job.templates import *
133-from lava.tool.command import Command, CommandGroup
134+from lava.job.templates import (
135+ BOOT_TEST,
136+)
137+from lava.tool.command import CommandGroup
138 from lava.tool.errors import CommandError
139-
140 from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend
141-import xmlrpclib
142+from lava_tool.utils import has_command
143+
144
145 class job(CommandGroup):
146- """
147- LAVA job file handling
148- """
149-
150+ """LAVA job file handling."""
151 namespace = 'lava.job.commands'
152
153-class BaseCommand(Command):
154-
155- def __init__(self, parser, args):
156- super(BaseCommand, self).__init__(parser, args)
157- self.config = InteractiveConfig(force_interactive=self.args.interactive)
158-
159- @classmethod
160- def register_arguments(cls, parser):
161- super(BaseCommand, cls).register_arguments(parser)
162- parser.add_argument(
163- "-i", "--interactive",
164- action='store_true',
165- help=("Forces asking for input parameters even if we already "
166- "have them cached."))
167
168 class new(BaseCommand):
169+ """Creates a new job file."""
170
171 @classmethod
172 def register_arguments(cls, parser):
173@@ -57,20 +51,22 @@
174 parser.add_argument("FILE", help=("Job file to be created."))
175
176 def invoke(self):
177- if exists(self.args.FILE):
178- raise CommandError('%s already exists' % self.args.FILE)
179+ if os.path.exists(self.args.FILE):
180+ raise CommandError('{0} already exists.'.format(self.args.FILE))
181
182- with open(self.args.FILE, 'w') as f:
183- job = Job(BOOT_TEST)
184- job.fill_in(self.config)
185- job.write(f)
186+ with open(self.args.FILE, 'w') as job_file:
187+ job_instance = Job(BOOT_TEST)
188+ job_instance.fill_in(self.config)
189+ job_instance.write(job_file)
190
191
192 class submit(BaseCommand):
193+ """Submits the specified job file."""
194+
195 @classmethod
196 def register_arguments(cls, parser):
197 super(submit, cls).register_arguments(parser)
198- parser.add_argument("FILE", help=("The job file to submit"))
199+ parser.add_argument("FILE", help=("The job file to submit."))
200
201 def invoke(self):
202 jobfile = self.args.FILE
203@@ -85,10 +81,61 @@
204 auth_backend=KeyringAuthBackend())
205 try:
206 job_id = server.scheduler.submit_job(jobdata)
207- print "Job submitted with job ID %d" % job_id
208- except xmlrpclib.Fault, e:
209- raise CommandError(str(e))
210+ print >> sys.stdout, "Job submitted with job ID {0}".format(job_id)
211+ except xmlrpclib.Fault, exc:
212+ raise CommandError(str(exc))
213+
214
215 class run(BaseCommand):
216+ """Runs the specified job file on the local dispatcher."""
217+
218+ @classmethod
219+ def register_arguments(cls, parser):
220+ super(run, cls).register_arguments(parser)
221+ parser.add_argument("FILE", help=("The job file to submit."))
222+
223+ @classmethod
224+ def _choose_device(cls, devices):
225+ """Let the user choose the device to use.
226+
227+ :param devices: The list of available devices.
228+ :return The selected device.
229+ """
230+ devices_len = len(devices)
231+ output_list = []
232+ for device, number in zip(devices, range(1, devices_len + 1)):
233+ output_list.append("\t{0}. {1}\n".format(number, device.hostname))
234+
235+ print >> sys.stdout, ("More than one local device found. "
236+ "Please choose one:\n")
237+ print >> sys.stdout, "".join(output_list)
238+
239+ while True:
240+ try:
241+ user_input = raw_input("Device number to use: ").strip()
242+
243+ if user_input in [str(x) for x in range(1, devices_len + 1)]:
244+ return devices[int(user_input) - 1].hostname
245+ else:
246+ continue
247+ except EOFError:
248+ user_input = None
249+ except KeyboardInterrupt:
250+ sys.exit(-1)
251+
252 def invoke(self):
253- print("hello world")
254+ if os.path.isfile(self.args.FILE):
255+ if has_command("lava-dispatch"):
256+ devices = self.get_devices()
257+ if devices:
258+ if len(devices) > 1:
259+ device = self._choose_device(devices)
260+ else:
261+ device = devices[0].hostname
262+ self.run(["lava-dispatch", "--target", device,
263+ self.args.FILE])
264+ else:
265+ raise CommandError("Cannot find lava-dispatcher installation.")
266+ else:
267+ raise CommandError("The file '{0}' does not exists. or is not "
268+ "a file.".format(self.args.FILE))
269
270=== modified file 'lava/job/tests/test_commands.py'
271--- lava/job/tests/test_commands.py 2013-06-03 18:06:49 +0000
272+++ lava/job/tests/test_commands.py 2013-06-17 09:01:27 +0000
273@@ -20,74 +20,108 @@
274 Unit tests for the commands classes
275 """
276
277-from argparse import ArgumentParser
278 import json
279-from os import (
280- makedirs,
281- removedirs,
282-)
283-from os.path import(
284- exists,
285- join,
286-)
287-from shutil import(
288- rmtree,
289-)
290-from tempfile import mkdtemp
291+import os
292+import shutil
293+import sys
294+import tempfile
295+
296+from mock import MagicMock, patch
297 from unittest import TestCase
298
299-from lava.config import NonInteractiveConfig
300-from lava.job.commands import *
301+from lava.config import NonInteractiveConfig, Parameter
302+
303+from lava.job.commands import (
304+ new,
305+ run,
306+)
307+
308 from lava.tool.errors import CommandError
309
310-from mocker import Mocker
311-
312-def make_command(command, *args):
313- parser = ArgumentParser(description="fake argument parser")
314- command.register_arguments(parser)
315- the_args = parser.parse_args(*args)
316- cmd = command(parser, the_args)
317- cmd.config = NonInteractiveConfig({ 'device_type': 'foo', 'prebuilt_image': 'bar' })
318- return cmd
319
320 class CommandTest(TestCase):
321
322 def setUp(self):
323- self.tmpdir = mkdtemp()
324+ # Fake the stdout.
325+ self.original_stdout = sys.stdout
326+ sys.stdout = open("/dev/null", "w")
327+ self.original_stderr = sys.stderr
328+ sys.stderr = open("/dev/null", "w")
329+ self.original_stdin = sys.stdin
330+
331+ self.device = "panda02"
332+
333+ self.tmpdir = tempfile.mkdtemp()
334+ self.tmpfile = tempfile.NamedTemporaryFile(delete=False)
335+ self.parser = MagicMock()
336+ self.args = MagicMock()
337+ self.args.interactive = MagicMock(return_value=False)
338+ self.args.FILE = self.tmpfile.name
339+
340+ self.device_type = Parameter('device_type')
341+ self.prebuilt_image = Parameter('prebuilt_image',
342+ depends=self.device_type)
343+ self.config = NonInteractiveConfig(
344+ {'device_type': 'foo', 'prebuilt_image': 'bar'})
345
346 def tearDown(self):
347- rmtree(self.tmpdir)
348+ sys.stdin = self.original_stdin
349+ sys.stdout = self.original_stdout
350+ sys.stderr = self.original_stderr
351+ os.unlink(self.tmpfile.name)
352+ shutil.rmtree(self.tmpdir)
353
354 def tmp(self, filename):
355- return join(self.tmpdir, filename)
356+ """Returns a path to a non existent file.
357+
358+ :param filename: The name the file should have.
359+ :return A path.
360+ """
361+ return os.path.join(self.tmpdir, filename)
362+
363
364 class JobNewTest(CommandTest):
365
366+ def setUp(self):
367+ super(JobNewTest, self).setUp()
368+ self.args.FILE = self.tmp("new_file.json")
369+ self.new_command = new(self.parser, self.args)
370+ self.new_command.config = self.config
371+
372+ def tearDown(self):
373+ super(JobNewTest, self).tearDown()
374+ if os.path.exists(self.args.FILE):
375+ os.unlink(self.args.FILE)
376+
377 def test_create_new_file(self):
378- f = self.tmp('file.json')
379- command = make_command(new, [f])
380- command.invoke()
381- self.assertTrue(exists(f))
382+ self.new_command.invoke()
383+ self.assertTrue(os.path.exists(self.args.FILE))
384
385 def test_fills_in_template_parameters(self):
386- f = self.tmp('myjob.json')
387- command = make_command(new, [f])
388- command.invoke()
389+ self.new_command.invoke()
390
391- data = json.loads(open(f).read())
392+ data = json.loads(open(self.args.FILE).read())
393 self.assertEqual(data['device_type'], 'foo')
394
395- def test_wont_overwriteexisting_file(self):
396- existing = self.tmp('existing.json')
397- with open(existing, 'w') as f:
398+ def test_wont_overwrite_existing_file(self):
399+ with open(self.args.FILE, 'w') as f:
400 f.write("CONTENTS")
401- command = make_command(new, [existing])
402- with self.assertRaises(CommandError):
403- command.invoke()
404- self.assertEqual("CONTENTS", open(existing).read())
405-
406-class JobSubmitTest(CommandTest):
407-
408- def test_receives_job_file_in_cmdline(self):
409- cmd = make_command(new, ['FOO.json'])
410- self.assertEqual('FOO.json', cmd.args.FILE)
411+
412+ self.assertRaises(CommandError, self.new_command.invoke)
413+ self.assertEqual("CONTENTS", open(self.args.FILE).read())
414+
415+
416+class JobRunTest(CommandTest):
417+
418+ def test_invoke_raises_0(self):
419+ # Users passes a non existing job file to the run command.
420+ self.args.FILE = self.tmp("test_invoke_raises_0.json")
421+ command = run(self.parser, self.args)
422+ self.assertRaises(CommandError, command.invoke)
423+
424+ @patch("lava.job.commands.has_command", new=MagicMock(return_value=False))
425+ def test_invoke_raises_1(self):
426+ # Users passes a valid file to the run command, but she does not have
427+ # the dispatcher installed.
428+ command = run(self.parser, self.args)
429+ self.assertRaises(CommandError, command.invoke)

Subscribers

People subscribed via source and target branches

to all changes: