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
=== added file 'lava/base_command.py'
--- lava/base_command.py 1970-01-01 00:00:00 +0000
+++ lava/base_command.py 2013-06-17 09:01:27 +0000
@@ -0,0 +1,80 @@
1# Copyright (C) 2013 Linaro Limited
2#
3# Author: Milo Casagrande <milo.casagrande@linaro.org>
4#
5# This file is part of lava-tool.
6#
7# lava-tool is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser General Public License version 3
9# as published by the Free Software Foundation
10#
11# lava-tool is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
18
19"""Base command class common to lava commands series."""
20
21import subprocess
22
23from lava.config import InteractiveConfig
24from lava.tool.command import Command
25from lava.tool.errors import CommandError
26
27
28class BaseCommand(Command):
29 """Base command class for all lava commands."""
30 def __init__(self, parser, args):
31 super(BaseCommand, self).__init__(parser, args)
32 self.config = InteractiveConfig(
33 force_interactive=self.args.interactive)
34
35 @classmethod
36 def register_arguments(cls, parser):
37 super(BaseCommand, cls).register_arguments(parser)
38 parser.add_argument("-i", "--interactive",
39 action='store_true',
40 help=("Forces asking for input parameters even if "
41 "we already have them cached."))
42
43 @classmethod
44 def get_dispatcher_paths(cls):
45 """Tries to get the dispatcher paths from lava-dispatcher.
46
47 :return A list of paths.
48 """
49 try:
50 from lava_dispatcher.config import write_path
51 return write_path()
52 except ImportError:
53 raise CommandError("Cannot find lava-dispatcher installation.")
54
55 @classmethod
56 def get_devices(cls):
57 """Gets the devices list from the dispatcher.
58
59 :return A list of DeviceConfig.
60 """
61 try:
62 from lava_dispatcher.config import get_devices
63 return get_devices()
64 except ImportError:
65 raise CommandError("Cannot find lava-dispatcher installation.")
66
67 @classmethod
68 def run(cls, cmd_args):
69 """Runs the supplied command args.
70
71 :param cmd_args: The command, and its optional arguments, to run.
72 :return The command execution return code.
73 """
74 if not isinstance(cmd_args, list):
75 cmd_args = list(cmd_args)
76 try:
77 return subprocess.check_call(cmd_args)
78 except subprocess.CalledProcessError:
79 raise CommandError("Error running the following command: "
80 "{0}".format(" ".join(cmd_args)))
081
=== modified file 'lava/job/__init__.py'
--- lava/job/__init__.py 2013-05-28 22:08:12 +0000
+++ lava/job/__init__.py 2013-06-17 09:01:27 +0000
@@ -21,12 +21,13 @@
2121
22from lava.job.templates import Parameter22from lava.job.templates import Parameter
2323
24
24class Job:25class Job:
25
26 def __init__(self, template):26 def __init__(self, template):
27 self.data = deepcopy(template)27 self.data = deepcopy(template)
2828
29 def fill_in(self, config):29 def fill_in(self, config):
30
30 def insert_data(data):31 def insert_data(data):
31 if isinstance(data, dict):32 if isinstance(data, dict):
32 keys = data.keys()33 keys = data.keys()
@@ -44,4 +45,3 @@
4445
45 def write(self, stream):46 def write(self, stream):
46 stream.write(json.dumps(self.data, indent=4))47 stream.write(json.dumps(self.data, indent=4))
47
4848
=== modified file 'lava/job/commands.py'
--- lava/job/commands.py 2013-06-03 18:06:49 +0000
+++ lava/job/commands.py 2013-06-17 09:01:27 +0000
@@ -16,40 +16,34 @@
16# You should have received a copy of the GNU Lesser General Public License16# You should have received a copy of the GNU Lesser General Public License
17# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.17# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
1818
19from os.path import exists19"""
2020LAVA job commands.
21from lava.config import InteractiveConfig21"""
22
23import os
24import sys
25import xmlrpclib
26
27from lava.base_command import BaseCommand
28
29from lava.config import Parameter
22from lava.job import Job30from lava.job import Job
23from lava.job.templates import *31from lava.job.templates import (
24from lava.tool.command import Command, CommandGroup32 BOOT_TEST,
33)
34from lava.tool.command import CommandGroup
25from lava.tool.errors import CommandError35from lava.tool.errors import CommandError
26
27from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend36from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend
28import xmlrpclib37from lava_tool.utils import has_command
38
2939
30class job(CommandGroup):40class job(CommandGroup):
31 """41 """LAVA job file handling."""
32 LAVA job file handling
33 """
34
35 namespace = 'lava.job.commands'42 namespace = 'lava.job.commands'
3643
37class BaseCommand(Command):
38
39 def __init__(self, parser, args):
40 super(BaseCommand, self).__init__(parser, args)
41 self.config = InteractiveConfig(force_interactive=self.args.interactive)
42
43 @classmethod
44 def register_arguments(cls, parser):
45 super(BaseCommand, cls).register_arguments(parser)
46 parser.add_argument(
47 "-i", "--interactive",
48 action='store_true',
49 help=("Forces asking for input parameters even if we already "
50 "have them cached."))
5144
52class new(BaseCommand):45class new(BaseCommand):
46 """Creates a new job file."""
5347
54 @classmethod48 @classmethod
55 def register_arguments(cls, parser):49 def register_arguments(cls, parser):
@@ -57,20 +51,22 @@
57 parser.add_argument("FILE", help=("Job file to be created."))51 parser.add_argument("FILE", help=("Job file to be created."))
5852
59 def invoke(self):53 def invoke(self):
60 if exists(self.args.FILE):54 if os.path.exists(self.args.FILE):
61 raise CommandError('%s already exists' % self.args.FILE)55 raise CommandError('{0} already exists.'.format(self.args.FILE))
6256
63 with open(self.args.FILE, 'w') as f:57 with open(self.args.FILE, 'w') as job_file:
64 job = Job(BOOT_TEST)58 job_instance = Job(BOOT_TEST)
65 job.fill_in(self.config)59 job_instance.fill_in(self.config)
66 job.write(f)60 job_instance.write(job_file)
6761
6862
69class submit(BaseCommand):63class submit(BaseCommand):
64 """Submits the specified job file."""
65
70 @classmethod66 @classmethod
71 def register_arguments(cls, parser):67 def register_arguments(cls, parser):
72 super(submit, cls).register_arguments(parser)68 super(submit, cls).register_arguments(parser)
73 parser.add_argument("FILE", help=("The job file to submit"))69 parser.add_argument("FILE", help=("The job file to submit."))
7470
75 def invoke(self):71 def invoke(self):
76 jobfile = self.args.FILE72 jobfile = self.args.FILE
@@ -85,10 +81,61 @@
85 auth_backend=KeyringAuthBackend())81 auth_backend=KeyringAuthBackend())
86 try:82 try:
87 job_id = server.scheduler.submit_job(jobdata)83 job_id = server.scheduler.submit_job(jobdata)
88 print "Job submitted with job ID %d" % job_id84 print >> sys.stdout, "Job submitted with job ID {0}".format(job_id)
89 except xmlrpclib.Fault, e:85 except xmlrpclib.Fault, exc:
90 raise CommandError(str(e))86 raise CommandError(str(exc))
87
9188
92class run(BaseCommand):89class run(BaseCommand):
90 """Runs the specified job file on the local dispatcher."""
91
92 @classmethod
93 def register_arguments(cls, parser):
94 super(run, cls).register_arguments(parser)
95 parser.add_argument("FILE", help=("The job file to submit."))
96
97 @classmethod
98 def _choose_device(cls, devices):
99 """Let the user choose the device to use.
100
101 :param devices: The list of available devices.
102 :return The selected device.
103 """
104 devices_len = len(devices)
105 output_list = []
106 for device, number in zip(devices, range(1, devices_len + 1)):
107 output_list.append("\t{0}. {1}\n".format(number, device.hostname))
108
109 print >> sys.stdout, ("More than one local device found. "
110 "Please choose one:\n")
111 print >> sys.stdout, "".join(output_list)
112
113 while True:
114 try:
115 user_input = raw_input("Device number to use: ").strip()
116
117 if user_input in [str(x) for x in range(1, devices_len + 1)]:
118 return devices[int(user_input) - 1].hostname
119 else:
120 continue
121 except EOFError:
122 user_input = None
123 except KeyboardInterrupt:
124 sys.exit(-1)
125
93 def invoke(self):126 def invoke(self):
94 print("hello world")127 if os.path.isfile(self.args.FILE):
128 if has_command("lava-dispatch"):
129 devices = self.get_devices()
130 if devices:
131 if len(devices) > 1:
132 device = self._choose_device(devices)
133 else:
134 device = devices[0].hostname
135 self.run(["lava-dispatch", "--target", device,
136 self.args.FILE])
137 else:
138 raise CommandError("Cannot find lava-dispatcher installation.")
139 else:
140 raise CommandError("The file '{0}' does not exists. or is not "
141 "a file.".format(self.args.FILE))
95142
=== modified file 'lava/job/tests/test_commands.py'
--- lava/job/tests/test_commands.py 2013-06-03 18:06:49 +0000
+++ lava/job/tests/test_commands.py 2013-06-17 09:01:27 +0000
@@ -20,74 +20,108 @@
20Unit tests for the commands classes20Unit tests for the commands classes
21"""21"""
2222
23from argparse import ArgumentParser
24import json23import json
25from os import (24import os
26 makedirs,25import shutil
27 removedirs,26import sys
28)27import tempfile
29from os.path import(28
30 exists,29from mock import MagicMock, patch
31 join,
32)
33from shutil import(
34 rmtree,
35)
36from tempfile import mkdtemp
37from unittest import TestCase30from unittest import TestCase
3831
39from lava.config import NonInteractiveConfig32from lava.config import NonInteractiveConfig, Parameter
40from lava.job.commands import *33
34from lava.job.commands import (
35 new,
36 run,
37)
38
41from lava.tool.errors import CommandError39from lava.tool.errors import CommandError
4240
43from mocker import Mocker
44
45def make_command(command, *args):
46 parser = ArgumentParser(description="fake argument parser")
47 command.register_arguments(parser)
48 the_args = parser.parse_args(*args)
49 cmd = command(parser, the_args)
50 cmd.config = NonInteractiveConfig({ 'device_type': 'foo', 'prebuilt_image': 'bar' })
51 return cmd
5241
53class CommandTest(TestCase):42class CommandTest(TestCase):
5443
55 def setUp(self):44 def setUp(self):
56 self.tmpdir = mkdtemp()45 # Fake the stdout.
46 self.original_stdout = sys.stdout
47 sys.stdout = open("/dev/null", "w")
48 self.original_stderr = sys.stderr
49 sys.stderr = open("/dev/null", "w")
50 self.original_stdin = sys.stdin
51
52 self.device = "panda02"
53
54 self.tmpdir = tempfile.mkdtemp()
55 self.tmpfile = tempfile.NamedTemporaryFile(delete=False)
56 self.parser = MagicMock()
57 self.args = MagicMock()
58 self.args.interactive = MagicMock(return_value=False)
59 self.args.FILE = self.tmpfile.name
60
61 self.device_type = Parameter('device_type')
62 self.prebuilt_image = Parameter('prebuilt_image',
63 depends=self.device_type)
64 self.config = NonInteractiveConfig(
65 {'device_type': 'foo', 'prebuilt_image': 'bar'})
5766
58 def tearDown(self):67 def tearDown(self):
59 rmtree(self.tmpdir)68 sys.stdin = self.original_stdin
69 sys.stdout = self.original_stdout
70 sys.stderr = self.original_stderr
71 os.unlink(self.tmpfile.name)
72 shutil.rmtree(self.tmpdir)
6073
61 def tmp(self, filename):74 def tmp(self, filename):
62 return join(self.tmpdir, filename)75 """Returns a path to a non existent file.
76
77 :param filename: The name the file should have.
78 :return A path.
79 """
80 return os.path.join(self.tmpdir, filename)
81
6382
64class JobNewTest(CommandTest):83class JobNewTest(CommandTest):
6584
85 def setUp(self):
86 super(JobNewTest, self).setUp()
87 self.args.FILE = self.tmp("new_file.json")
88 self.new_command = new(self.parser, self.args)
89 self.new_command.config = self.config
90
91 def tearDown(self):
92 super(JobNewTest, self).tearDown()
93 if os.path.exists(self.args.FILE):
94 os.unlink(self.args.FILE)
95
66 def test_create_new_file(self):96 def test_create_new_file(self):
67 f = self.tmp('file.json')97 self.new_command.invoke()
68 command = make_command(new, [f])98 self.assertTrue(os.path.exists(self.args.FILE))
69 command.invoke()
70 self.assertTrue(exists(f))
7199
72 def test_fills_in_template_parameters(self):100 def test_fills_in_template_parameters(self):
73 f = self.tmp('myjob.json')101 self.new_command.invoke()
74 command = make_command(new, [f])
75 command.invoke()
76102
77 data = json.loads(open(f).read())103 data = json.loads(open(self.args.FILE).read())
78 self.assertEqual(data['device_type'], 'foo')104 self.assertEqual(data['device_type'], 'foo')
79105
80 def test_wont_overwriteexisting_file(self):106 def test_wont_overwrite_existing_file(self):
81 existing = self.tmp('existing.json')107 with open(self.args.FILE, 'w') as f:
82 with open(existing, 'w') as f:
83 f.write("CONTENTS")108 f.write("CONTENTS")
84 command = make_command(new, [existing])109
85 with self.assertRaises(CommandError):110 self.assertRaises(CommandError, self.new_command.invoke)
86 command.invoke()111 self.assertEqual("CONTENTS", open(self.args.FILE).read())
87 self.assertEqual("CONTENTS", open(existing).read())112
88113
89class JobSubmitTest(CommandTest):114class JobRunTest(CommandTest):
90115
91 def test_receives_job_file_in_cmdline(self):116 def test_invoke_raises_0(self):
92 cmd = make_command(new, ['FOO.json'])117 # Users passes a non existing job file to the run command.
93 self.assertEqual('FOO.json', cmd.args.FILE)118 self.args.FILE = self.tmp("test_invoke_raises_0.json")
119 command = run(self.parser, self.args)
120 self.assertRaises(CommandError, command.invoke)
121
122 @patch("lava.job.commands.has_command", new=MagicMock(return_value=False))
123 def test_invoke_raises_1(self):
124 # Users passes a valid file to the run command, but she does not have
125 # the dispatcher installed.
126 command = run(self.parser, self.args)
127 self.assertRaises(CommandError, command.invoke)

Subscribers

People subscribed via source and target branches

to all changes: