Merge lp:~milo/lava-tool/lava-165 into lp:~linaro-validation/lava-tool/trunk

Proposed by Milo Casagrande
Status: Merged
Merged at revision: 188
Proposed branch: lp:~milo/lava-tool/lava-165
Merge into: lp:~linaro-validation/lava-tool/trunk
Prerequisite: lp:~milo/lava-tool/device-parameters
Diff against target: 3405 lines (+2196/-459)
30 files modified
.coveragerc (+1/-0)
entry_points.ini (+8/-0)
lava/commands.py (+240/-0)
lava/config.py (+50/-17)
lava/device/__init__.py (+3/-3)
lava/device/commands.py (+8/-6)
lava/device/tests/test_commands.py (+25/-18)
lava/device/tests/test_device.py (+16/-11)
lava/helper/command.py (+137/-60)
lava/helper/template.py (+80/-0)
lava/helper/tests/helper_test.py (+24/-4)
lava/helper/tests/test_command.py (+0/-89)
lava/helper/tests/test_template.py (+102/-0)
lava/job/__init__.py (+40/-12)
lava/job/commands.py (+21/-77)
lava/job/templates.py (+84/-19)
lava/job/tests/test_commands.py (+2/-9)
lava/job/tests/test_job.py (+40/-29)
lava/parameter.py (+199/-17)
lava/testdef/__init__.py (+60/-0)
lava/testdef/commands.py (+72/-0)
lava/testdef/templates.py (+75/-0)
lava/testdef/tests/test_commands.py (+153/-0)
lava/tests/test_commands.py (+128/-0)
lava/tests/test_config.py (+59/-65)
lava/tests/test_parameter.py (+92/-11)
lava_tool/tests/__init__.py (+13/-10)
lava_tool/tests/test_utils.py (+177/-2)
lava_tool/utils.py (+286/-0)
setup.py (+1/-0)
To merge this branch: bzr merge lp:~milo/lava-tool/lava-165
Reviewer Review Type Date Requested Status
Antonio Terceiro Pending
Review via email: mp+174942@code.launchpad.net

Description of the change

This adds final work towards completing the lava helper tools for card-15.
The merge adds the following commands:

lava init [DIR]
lava run [JOB|DIR]
lava submit [JOB|DIR]
lava update [JOB|DIR]

The "init" command initializes a directory with a job definition file, and a default subdirectory called "tests/" that will contain the test definition file(s), and a default shell script, called "mytest.sh". The test definition file steps, by default, will execute the shell script. The user is prompted to fill in the shell script.

The "run" command runs a job file on the local dispatcher, the "submit" one will send the job file to a remote LAVA server.

The "update" command can be used, in case the user modifies the content of the "tests/" directory, to update the job file.

The "run", "submit" and "update" commands can accept a job file (.json extension), a directory (it will pick up the first .json file) or nothing to use the current working directory.

New Parameter class have been introduced, not used at this stage, but where used with the first implementation. New attributes have also been added in order not to store the parameter on disk at program exit.

Tests have been added to cover most of the new features.

To post a comment you must log in.
lp:~milo/lava-tool/lava-165 updated
389. By Milo Casagrande

Updated copyright info, fixed typo.

390. By Milo Casagrande

Reworked the default job tempalte.

    * Remove the UrlListParameter and introduced the TarRepoParameter.
    * Reworked the key of the job templates.
    * Fixed the tests.

391. By Milo Casagrande

Fixed testdef_repos parameter: it is an array.

392. By Milo Casagrande

Fixed template access.

393. By Milo Casagrande

New functions to get and set a key in a template.

     * Added new tests too.

394. By Milo Casagrande

Used device_type, not target.

395. By Milo Casagrande

Make sure shell script is executable.

    * Removed unused imports.

396. By Milo Casagrande

Fixed docstring.

397. By Milo Casagrande

Removed unused imports.

398. By Milo Casagrande

Added submit_results command.

    * Added new step in job template to submit the results.
      It will ask two parameters: the stream and the server.
    * Fixed timeout value in lava_test_shell command.

399. By Milo Casagrande

Added default parse parttern for testdef, updated tests.

Revision history for this message
Antonio Terceiro (terceiro) wrote :
Download full text (57.8 KiB)

On Tue, Jul 16, 2013 at 07:41:29AM -0000, Milo Casagrande wrote:
> Milo Casagrande has proposed merging lp:~milo/lava-tool/lava-165 into lp:lava-tool with lp:~milo/lava-tool/device-parameters as a prerequisite.
>
> Requested reviews:
> Antonio Terceiro (terceiro)
>
> For more details, see:
> https://code.launchpad.net/~milo/lava-tool/lava-165/+merge/174942
>
> This adds final work towards completing the lava helper tools for card-15.
> The merge adds the following commands:
>
> lava init [DIR]
> lava run [JOB|DIR]
> lava submit [JOB|DIR]
> lava update [JOB|DIR]
>
> The "init" command initializes a directory with a job definition file, and a default subdirectory called "tests/" that will contain the test definition file(s), and a default shell script, called "mytest.sh". The test definition file steps, by default, will execute the shell script. The user is prompted to fill in the shell script.
>
> The "run" command runs a job file on the local dispatcher, the "submit" one will send the job file to a remote LAVA server.
>
> The "update" command can be used, in case the user modifies the content of the "tests/" directory, to update the job file.
>
> The "run", "submit" and "update" commands can accept a job file (.json extension), a directory (it will pick up the first .json file) or nothing to use the current working directory.
>
> New Parameter class have been introduced, not used at this stage, but where used with the first implementation. New attributes have also been added in order not to store the parameter on disk at program exit.
>
> Tests have been added to cover most of the new features.

arf ... this one was big!

> === added file 'lava/commands.py'
> --- lava/commands.py 1970-01-01 00:00:00 +0000
> +++ lava/commands.py 2013-07-22 09:32:26 +0000
> @@ -0,0 +1,293 @@
> +# 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://www.gnu.org/licenses/>.
> +
> +"""
> +Lava init commands.
> +
> +When invoking:
> +
> + `lava init [DIR]`
> +
> +the command will create a default directory and files structure as follows:
> +
> +DIR/
> + |
> + +- JOB_FILE.json
> + +- tests/
> + |
> + + mytest.sh
> + + lavatest.yaml
> +
> +If DIR is not passed, it will use the current working directory.
> +JOB_FILE is a file name that will be asked to the user, along with
> +other necessary information to define the tests.
> +
> +If the user manually updates either the lavatest.yaml or mytest.sh file, it is
> +necessary to run the following command in order t...

Revision history for this message
Milo Casagrande (milo) wrote :
Download full text (12.0 KiB)

On Wed, Jul 24, 2013 at 2:49 AM, Antonio Terceiro
<email address hidden> wrote:
>> +
>> +INIT_TEMPLATE = {
>> + JOBFILE_ID: JOBFILE_PARAMETER,
>> +}
>
> Do we really need a dictionary with a single key?

If we want to maintain the same pattern as for the other commands,
yes: go through the template, look up the parameter in the config, and
ask for the values there.
Or we need to change the "expand_template" function signature and also
the name to accept single Parameter objects.

>> + def _create_files(self, data, full_path, test_path):
>> + # This is the default script file as defined in the testdef template.
>> + default_script = os.path.join(test_path, DEFAULT_TESTDEF_SCRIPT)
>> +
>> + if not os.path.isfile(default_script):
>> + # We do not have the default testdef script. Create it, but
>> + # remind the user to update it.
>> + print >> sys.stdout, ("\nCreating default test script "
>> + "'{0}'.".format(DEFAULT_TESTDEF_SCRIPT))
>> +
>> + with open(default_script, "w") as write_file:
>> + write_file.write(DEFAULT_TESTDEF_SCRIPT_CONTENT)
>
> if this behaviour of "create a file with this content here" starts to repeat,
> we probably want to encapsulatet it somewhere else (I noted it at least once in
> other parts of this MP).

Sounds good, even if at the moment should be only used here.
It was used also in the tests to fill up files with fake content.

>> + # Prompt the user to write the script file.
>> + self.edit_file(default_script)
>> +
>> + # Make sure the script is executable.
>> + os.chmod(default_script,
>> + stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
>> +
>> + print >> sys.stdout, ("\nCreating test definition "
>> + "'{0}':".format(DEFAULT_TESTDEF_FILE))
>
> print already writes to stdout by default. is it really necessary to be that explicit?

I used the same behavior found in most of lava-tool (in particular in
lava_dashboard_tool) where it is using the explicit redirect.
Albeit, there is a mixed use of it even in the old code.

> I’m not sure about this approach of calling other commands. IMO the best
> approach would be to concentrate as much of the logic as possible in the
> model/backend classes, them call them from both commands.

Some refactoring will be needed, not sure if in this merge proposal though.

> IIUC you are assuming that you will be always storing the tarball in the job
> file?

True only if you go through the "lava init" step.
If you want to use the other commands, this does not happen. It might
need some refactoring since "testdef|script submit" is still missing
though.

> My initial thinking was to always generate a tarball dinamically, so that
> you don’t have to explicitly update a job file. It would go like this:
>
> $ lava job submit FOO.json
>
> submits FOO.json as ir

This works.

> $ lava testdef submit FOO.yaml
>
> creates the tarball and a job file dinamically; submit those
>
> $ lava script submit FOO.sh
>
> creates testdef, tarball and job file dynamically; submit th...

Revision history for this message
Antonio Terceiro (terceiro) wrote :
Download full text (3.6 KiB)

> > My initial thinking was to always generate a tarball dinamically, so that
> > you don’t have to explicitly update a job file. It would go like this:
> >
> > $ lava job submit FOO.json
> >
> > submits FOO.json as ir
>
> This works.
>
> > $ lava testdef submit FOO.yaml
> >
> > creates the tarball and a job file dinamically; submit those
> >
> > $ lava script submit FOO.sh
> >
> > creates testdef, tarball and job file dynamically; submit them
>
> These two are still missing.
>
> After showing this at Connect though, the common patterns were:
> - I will not use this (mostly from QA or people that knows how to write testdef)
> - There are too many commands (from a couple of Linaro employees and
> ARM ones too, hence the "lava init" stuff that will do everything
> almost automatically).

OK, that's good to know. I think we can reduce the number of commands,
but still keep the functionality - because craeting testdefs
automatically is *not* intended at people who already know how to create
them by themselves. :-)

> >> - user_input = raw_input(prompt).strip()
> >> + data = raw_input(prompt).strip()
> >> except EOFError:
> >> - pass
> >> + # Force to return None.
> >> + data = None
> >> except KeyboardInterrupt:
> >> sys.exit(-1)
> >> -
> >> - if user_input is not None:
> >> + return data
> >> +
> >> + @classmethod
> >> + def serialize(cls, value):
> >> + """Serializes the passed value to be friendly written to file.
> >> +
> >> + Lists are serialized as a comma separated string of values.
> >
> > I already saw something about serializing lists above. Can we keep all this
> > list serialization/deserialization just here?
>
> What do you mean? Having a single method to serialize and deserialize
> instead of the two we have right now?
> "serialize" and "deserialize" are in the same class (Parameter), the
> LIST_SERIALIZE_DELIMITER is a module constant.

Above there was a call to serialize, and maybe this should be handled
transparently inside this class. The factoring in serialize/deserialize
is good already.

> >> + :param value: The value to serialize.
> >> + :return The serialized value as string.
> >> + """
> >> + serialized = ""
> >> + if isinstance(value, list):
> >> + serialized = LIST_SERIALIZE_DELIMITER.join(
> >> + str(x) for x in value if x)
> >> + else:
> >> + serialized = str(value)
> >> + return serialized
> >
> > I wonder if we can we use a existing serialization instead of using a arbitrary
> > delimiter ... e.g. json + base64, this way we don’t have to limit ourselves to
> > values that do not contain a comma. But OTOH we want something that is readable
> > (and editable!) in the config file ...
>
> I wanted to use something that could have been human readable
> (avoiding also all hashing mechanism), and easily to modify.
> I considered also pickle, but then I would have preferred to pass
> everything through it rather than just a small set of values.
> We can probably use just json.dumps() and json.loads(), even if ...

Read more...

Revision history for this message
Milo Casagrande (milo) wrote :

On Wed, Jul 24, 2013 at 3:16 PM, Antonio Terceiro
<email address hidden> wrote:
> Agreed. We only have to consider whether we expect people to edit that
> file manually or not, because the serialization of the entire file in
> either JSON or YAML might mess up with the order in which a user would
> write the configuration.

I wouldn't be much concerned with how a user would write it. As long
as the file is valid JSON/YAML, we should be safe using it.
When it is written it is subject to changes anyway.

If we start considering it a cache, well, I do not even expect the
user to fiddle with it. :-)

> But maybe in the end what we are calling configuration is actually just
> a cache of pre-answered questions instead of an actual configuration
> file ...

It is actually, only two of those values I can call "configuration":
the server and the rpc_endpoint to talk to a remote server.
All the others, not really configuration values.

But still, even for those two, I would not expect to be asked each
time to insert or confirm them. A configuration for me is something
that should not be asked each time.

--
Milo Casagrande | Automation Engineer
Linaro.org <www.linaro.org> │ Open source software for ARM SoCs

lp:~milo/lava-tool/lava-165 updated
400. By Milo Casagrande

Removed unused UrlSchemeParameter.

401. By Milo Casagrande

Fixed doc string and behaviour for the retrieve_file method.

402. By Milo Casagrande

Fixed doc string.

403. By Milo Casagrande

Fixed exception handling.

404. By Milo Casagrande

Use single quotes to avoid escaping.

405. By Milo Casagrande

Added lazy property for Config config_file, and fixed tests.

406. By Milo Casagrande

Fixed doc string in test method.

407. By Milo Casagrande

Removed constants.

408. By Milo Casagrande

Removed singleton, added getter and setter, fixed tests.

409. By Milo Casagrande

Fixed template helper functions.

410. By Milo Casagrande

Refactored testdef init, refactored utils functions, fixed tests.

411. By Milo Casagrande

Refactored methods and functions, fixed tests.

    * Refactored job templates.
    * Removed TarRepoParameter.

412. By Milo Casagrande

Fixed function call: missing parameter.

413. By Milo Casagrande

Removed job_ids save on file, removed constants.

414. By Milo Casagrande

Refacotred Job class.

    * Introduced a new cmd arg "--typ" to specify the kind of job type
      to create.
    * Fixed tests.

415. By Milo Casagrande

Completed utils, run and submit refactoring.

     * Fixed tests.

416. By Milo Casagrande

Removed constants.

417. By Milo Casagrande

Fixed help string.

418. By Milo Casagrande

Refactored the lava init command.

419. By Milo Casagrande

Fixed tests.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.coveragerc'
2--- .coveragerc 2013-07-25 15:43:25 +0000
3+++ .coveragerc 2013-07-25 15:43:26 +0000
4@@ -3,6 +3,7 @@
5 source = .
6 omit =
7 setup*
8+ */tests/*
9
10 [report]
11 precision = 2
12
13=== modified file 'entry_points.ini'
14--- entry_points.ini 2013-07-25 15:43:25 +0000
15+++ entry_points.ini 2013-07-25 15:43:26 +0000
16@@ -9,6 +9,11 @@
17 dashboard = lava_dashboard_tool.commands:dashboard
18 job = lava.job.commands:job
19 device = lava.device.commands:device
20+testdef = lava.testdef.commands:testdef
21+init = lava.commands:init
22+submit = lava.commands:submit
23+run = lava.commands:run
24+update = lava.commands:update
25
26 [lava_tool.commands]
27 help = lava.tool.commands.help:help
28@@ -78,3 +83,6 @@
29 add = lava.device.commands:add
30 remove = lava.device.commands:remove
31 config = lava.device.commands:config
32+
33+[lava.testdef.commands]
34+new = lava.testdef.commands:new
35
36=== added file 'lava/commands.py'
37--- lava/commands.py 1970-01-01 00:00:00 +0000
38+++ lava/commands.py 2013-07-25 15:43:26 +0000
39@@ -0,0 +1,240 @@
40+# Copyright (C) 2013 Linaro Limited
41+#
42+# Author: Milo Casagrande <milo.casagrande@linaro.org>
43+#
44+# This file is part of lava-tool.
45+#
46+# lava-tool is free software: you can redistribute it and/or modify
47+# it under the terms of the GNU Lesser General Public License version 3
48+# as published by the Free Software Foundation
49+#
50+# lava-tool is distributed in the hope that it will be useful,
51+# but WITHOUT ANY WARRANTY; without even the implied warranty of
52+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
53+# GNU General Public License for more details.
54+#
55+# You should have received a copy of the GNU Lesser General Public License
56+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
57+
58+"""
59+Lava init commands.
60+
61+When invoking:
62+
63+ `lava init [DIR]`
64+
65+the command will create a default directory and files structure as follows:
66+
67+DIR/
68+ |
69+ +- JOB_FILE.json
70+ +- tests/
71+ |
72+ + mytest.sh
73+ + lavatest.yaml
74+
75+If DIR is not passed, it will use the current working directory.
76+JOB_FILE is a file name that will be asked to the user, along with
77+other necessary information to define the tests.
78+
79+If the user manually updates either the lavatest.yaml or mytest.sh file, it is
80+necessary to run the following command in order to update the job definition:
81+
82+ `lava update [JOB|DIR]`
83+"""
84+
85+import copy
86+import json
87+import os
88+import stat
89+import sys
90+
91+from lava.helper.command import BaseCommand
92+from lava.helper.template import (
93+ expand_template,
94+ set_value
95+)
96+from lava.job import (
97+ JOB_FILE_EXTENSIONS,
98+)
99+from lava.job.templates import (
100+ LAVA_TEST_SHELL_TAR_REPO_KEY,
101+)
102+from lava.parameter import (
103+ Parameter,
104+)
105+from lava.testdef.templates import (
106+ DEFAULT_TESTDEF_FILE,
107+ DEFAULT_TESTDEF_SCRIPT,
108+ DEFAULT_TESTDEF_SCRIPT_CONTENT,
109+)
110+from lava.tool.errors import CommandError
111+from lava_tool.utils import (
112+ edit_file,
113+ retrieve_file,
114+ create_dir,
115+ write_file,
116+)
117+
118+# Default directory structure name.
119+TESTS_DIR = "tests"
120+
121+# Internal parameter ids.
122+JOBFILE_ID = "jobfile"
123+
124+JOBFILE_PARAMETER = Parameter(JOBFILE_ID)
125+JOBFILE_PARAMETER.store = False
126+
127+INIT_TEMPLATE = {
128+ JOBFILE_ID: JOBFILE_PARAMETER,
129+}
130+
131+
132+class init(BaseCommand):
133+ """Set-ups the base directory structure."""
134+
135+ @classmethod
136+ def register_arguments(cls, parser):
137+ super(init, cls).register_arguments(parser)
138+ parser.add_argument("DIR",
139+ help=("The name of the directory to initialize. "
140+ "Defaults to current working directory."),
141+ nargs="?",
142+ default=os.getcwd())
143+
144+ def invoke(self):
145+ full_path = os.path.abspath(self.args.DIR)
146+
147+ if os.path.isfile(full_path):
148+ raise CommandError("'{0}' already exists, and is a "
149+ "file.".format(self.args.DIR))
150+
151+ create_dir(full_path)
152+
153+ data = self._update_data()
154+
155+ test_path = create_dir(full_path, TESTS_DIR)
156+ # TODO
157+ self._create_script(test_path)
158+
159+ testdef_file = self.create_test_definition(
160+ os.path.join(test_path, DEFAULT_TESTDEF_FILE))
161+
162+ job = data[JOBFILE_ID]
163+ self.create_tar_repo_job(
164+ os.path.join(full_path, job), testdef_file, test_path)
165+
166+ def _update_data(self):
167+ """Updates the template and ask values to the user.
168+
169+ The template in this case is a layout of the directory structure as it
170+ would be written to disk.
171+
172+ :return A dictionary containing all the necessary file names to create.
173+ """
174+ data = copy.deepcopy(INIT_TEMPLATE)
175+ expand_template(data, self.config)
176+
177+ return data
178+
179+ def _create_script(self, test_path):
180+ # This is the default script file as defined in the testdef template.
181+ default_script = os.path.join(test_path, DEFAULT_TESTDEF_SCRIPT)
182+
183+ if not os.path.isfile(default_script):
184+ # We do not have the default testdef script. Create it, but
185+ # remind the user to update it.
186+ print >> sys.stdout, ("\nCreating default test script "
187+ "'{0}'.".format(DEFAULT_TESTDEF_SCRIPT))
188+
189+ write_file(default_script, DEFAULT_TESTDEF_SCRIPT_CONTENT)
190+
191+ # Prompt the user to write the script file.
192+ edit_file(default_script)
193+
194+ # Make sure the script is executable.
195+ os.chmod(default_script,
196+ stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
197+
198+
199+class run(BaseCommand):
200+ """Runs a job on the local dispatcher."""
201+
202+ @classmethod
203+ def register_arguments(cls, parser):
204+ super(run, cls).register_arguments(parser)
205+ parser.add_argument("JOB",
206+ help=("The job file to run, or a directory "
207+ "containing a job file. If nothing is "
208+ "passed, it uses the current working "
209+ "directory."),
210+ nargs="?",
211+ default=os.getcwd())
212+
213+ def invoke(self):
214+ full_path = os.path.abspath(self.args.JOB)
215+ job_file = retrieve_file(full_path, JOB_FILE_EXTENSIONS)
216+
217+ super(run, self).run(job_file)
218+
219+
220+class submit(BaseCommand):
221+ """Submits a job to LAVA."""
222+
223+ @classmethod
224+ def register_arguments(cls, parser):
225+ super(submit, cls).register_arguments(parser)
226+ parser.add_argument("JOB",
227+ help=("The job file to send, or a directory "
228+ "containing a job file. If nothing is "
229+ "passed, it uses the current working "
230+ "directory."),
231+ nargs="?",
232+ default=os.getcwd())
233+
234+ def invoke(self):
235+ full_path = os.path.abspath(self.args.JOB)
236+ job_file = self.retrieve_file(full_path, JOB_FILE_EXTENSIONS)
237+
238+ super(submit, self).submit(job_file)
239+
240+
241+class update(BaseCommand):
242+ """Updates a job file with the correct data."""
243+
244+ @classmethod
245+ def register_arguments(cls, parser):
246+ super(update, cls).register_arguments(parser)
247+ parser.add_argument("JOB",
248+ help=("Automatically updates a job file "
249+ "definition. If nothing is passed, it uses"
250+ "the current working directory."),
251+ nargs="?",
252+ default=os.getcwd())
253+
254+ def invoke(self):
255+ full_path = os.path.abspath(self.args.JOB)
256+ job_file = self.retrieve_file(full_path, JOB_FILE_EXTENSIONS)
257+ job_dir = os.path.dirname(job_file)
258+ tests_dir = os.path.join(job_dir, TESTS_DIR)
259+
260+ if os.path.isdir(tests_dir):
261+ # TODO
262+ encoded_tests = None
263+
264+ json_data = None
265+ with open(job_file, "r") as json_file:
266+ try:
267+ json_data = json.load(json_file)
268+ set_value(
269+ json_data, LAVA_TEST_SHELL_TAR_REPO_KEY, encoded_tests)
270+ except Exception:
271+ raise CommandError("Cannot read job file '{0}'.".format(
272+ job_file))
273+
274+ content = json.dumps(json_data, indent=4)
275+ write_file(job_file, content)
276+
277+ print >> sys.stdout, "Job definition updated."
278+ else:
279+ raise CommandError("Cannot find tests directory.")
280
281=== modified file 'lava/config.py'
282--- lava/config.py 2013-07-25 15:43:25 +0000
283+++ lava/config.py 2013-07-25 15:43:26 +0000
284@@ -30,8 +30,10 @@
285 NoSectionError,
286 )
287
288+from lava.parameter import Parameter
289 from lava.tool.errors import CommandError
290
291+
292 __all__ = ['Config', 'InteractiveConfig']
293
294 # Store for function calls to be made at exit time.
295@@ -53,19 +55,35 @@
296 call()
297 atexit.register(_run_at_exit)
298
299-
300 class Config(object):
301 """A generic config object."""
302+
303 def __init__(self):
304 # The cache where to store parameters.
305 self._cache = {}
306- self._config_file = (os.environ.get('LAVACONFIG') or
307- os.path.join(os.path.expanduser('~'),
308- '.lavaconfig'))
309- self._config_backend = ConfigParser()
310- self._config_backend.read([self._config_file])
311+ self._config_file = None
312+ self._config_backend = None
313 AT_EXIT_CALLS.add(self.save)
314
315+ @property
316+ def config_file(self):
317+ if self._config_file is None:
318+ self._config_file = (os.environ.get('LAVACONFIG') or
319+ os.path.join(os.path.expanduser('~'),
320+ '.lavaconfig'))
321+ return self._config_file
322+
323+ @config_file.setter
324+ def config_file(self, value):
325+ self._config_file = value
326+
327+ @property
328+ def config_backend(self):
329+ if self._config_backend is None:
330+ self._config_backend = ConfigParser()
331+ self._config_backend.read([self.config_file])
332+ return self._config_backend
333+
334 def _calculate_config_section(self, parameter):
335 """Calculates the config section of the specified parameter.
336
337@@ -92,7 +110,7 @@
338 if not section:
339 section = self._calculate_config_section(parameter)
340 # Try to get the parameter value first if it has one.
341- if parameter.value:
342+ if parameter.value is not None:
343 value = parameter.value
344 else:
345 value = self._get_from_cache(parameter, section)
346@@ -109,7 +127,7 @@
347 """
348 value = None
349 try:
350- value = self._config_backend.get(section, parameter.id)
351+ value = self.config_backend.get(section, parameter.id)
352 except (NoOptionError, NoSectionError):
353 # Ignore, we return None.
354 pass
355@@ -147,10 +165,17 @@
356 :param value: The value to add.
357 :param section: The name of the section as in the config file.
358 """
359- if (not self._config_backend.has_section(section) and
360+ if (not self.config_backend.has_section(section) and
361 section != DEFAULT_SECTION):
362- self._config_backend.add_section(section)
363- self._config_backend.set(section, key, value)
364+ self.config_backend.add_section(section)
365+
366+ # This is done to serialize a list when ConfigParser is written to
367+ # file. Since there is no real support for list in ConfigParser, we
368+ # serialized it in a common way that can get easily deserialized.
369+ if isinstance(value, list):
370+ value = Parameter.serialize(value)
371+
372+ self.config_backend.set(section, key, value)
373 # Store in the cache too.
374 self._put_in_cache(key, value, section)
375
376@@ -158,6 +183,7 @@
377 """Adds a Parameter to the config file and cache.
378
379 :param Parameter: The parameter to add.
380+ :type Parameter
381 :param value: The value of the parameter. Defaults to None.
382 :param section: The section where this parameter should be stored.
383 Defaults to None.
384@@ -174,8 +200,8 @@
385
386 def save(self):
387 """Saves the config to file."""
388- with open(self._config_file, "w") as write_file:
389- self._config_backend.write(write_file)
390+ with open(self.config_file, "w") as write_file:
391+ self.config_backend.write(write_file)
392
393
394 class InteractiveConfig(Config):
395@@ -188,6 +214,14 @@
396 super(InteractiveConfig, self).__init__()
397 self._force_interactive = force_interactive
398
399+ @property
400+ def force_interactive(self):
401+ return self._force_interactive
402+
403+ @force_interactive.setter
404+ def force_interactive(self, value):
405+ self._force_interactive = value
406+
407 def get(self, parameter, section=None):
408 """Overrides the parent one.
409
410@@ -198,10 +232,9 @@
411 section = self._calculate_config_section(parameter)
412 value = super(InteractiveConfig, self).get(parameter, section)
413
414- if not (value is not None and parameter.asked):
415- if not value or self._force_interactive:
416- value = parameter.prompt(old_value=value)
417+ if value is None or self.force_interactive:
418+ value = parameter.prompt(old_value=value)
419
420- if value is not None:
421+ if value is not None and parameter.store:
422 self.put(parameter.id, value, section)
423 return value
424
425=== modified file 'lava/device/__init__.py'
426--- lava/device/__init__.py 2013-07-25 15:43:25 +0000
427+++ lava/device/__init__.py 2013-07-25 15:43:26 +0000
428@@ -67,8 +67,8 @@
429 # given on the command line for the config file.
430 if self.hostname is not None:
431 # We do not ask the user again this parameter.
432+ self.data[HOSTNAME_PARAMETER.id].value = self.hostname
433 self.data[HOSTNAME_PARAMETER.id].asked = True
434- config.put(HOSTNAME_PARAMETER.id, self.hostname)
435
436 expand_template(self.data, config)
437
438@@ -85,9 +85,9 @@
439 :param name: The name of the device we want matched to a real device.
440 :return A Device instance.
441 """
442- instance = Device(DEFAULT_TEMPLATE, name)
443+ instance = Device(DEFAULT_TEMPLATE, hostname=name)
444 for known_dev, (matcher, dev_template) in KNOWN_DEVICES.iteritems():
445 if matcher.match(name):
446- instance = Device(dev_template, name)
447+ instance = Device(dev_template, hostname=name)
448 break
449 return instance
450
451=== modified file 'lava/device/commands.py'
452--- lava/device/commands.py 2013-07-25 15:43:25 +0000
453+++ lava/device/commands.py 2013-07-25 15:43:26 +0000
454@@ -23,18 +23,20 @@
455 import os
456 import sys
457
458+from lava.device import get_known_device
459 from lava.helper.command import (
460 BaseCommand,
461 )
462-
463 from lava.helper.dispatcher import (
464 get_device_file,
465 get_devices_path,
466 )
467-
468-from lava.device import get_known_device
469 from lava.tool.command import CommandGroup
470 from lava.tool.errors import CommandError
471+from lava_tool.utils import (
472+ can_edit_file,
473+ edit_file,
474+)
475
476 DEVICE_FILE_SUFFIX = "conf"
477
478@@ -73,7 +75,7 @@
479
480 print >> sys.stdout, ("Created device file '{0}' in: {1}".format(
481 real_file_name, devices_path))
482- self.edit_file(device_conf_file)
483+ edit_file(device_conf_file)
484
485
486 class remove(BaseCommand):
487@@ -114,7 +116,7 @@
488 real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX])
489 device_conf = get_device_file(real_file_name)
490
491- if device_conf and self.can_edit_file(device_conf):
492- self.edit_file(device_conf)
493+ if device_conf and can_edit_file(device_conf):
494+ edit_file(device_conf)
495 else:
496 raise CommandError("Cannot edit file '{0}'".format(real_file_name))
497
498=== modified file 'lava/device/tests/test_commands.py'
499--- lava/device/tests/test_commands.py 2013-07-25 15:43:25 +0000
500+++ lava/device/tests/test_commands.py 2013-07-25 15:43:26 +0000
501@@ -50,35 +50,39 @@
502 name, args, kwargs = self.parser.method_calls[1]
503 self.assertIn("DEVICE", args)
504
505- @patch("lava.device.Device.__str__", new=MagicMock(return_value=""))
506- @patch("lava.device.Device.update", new=MagicMock())
507- @patch("lava.device.commands.get_device_file",
508- new=MagicMock(return_value=None))
509+ @patch("lava.device.commands.edit_file", create=True)
510+ @patch("lava.device.Device.__str__")
511+ @patch("lava.device.Device.update")
512+ @patch("lava.device.commands.get_device_file")
513 @patch("lava.device.commands.get_devices_path")
514- def test_add_invoke_0(self, get_devices_path_mock):
515+ def test_add_invoke_0(self, mocked_get_devices_path,
516+ mocked_get_device_file, mocked_update, mocked_str,
517+ mocked_edit_file):
518 # Tests invocation of the add command. Verifies that the conf file is
519 # written to disk.
520- get_devices_path_mock.return_value = self.temp_dir
521+ mocked_get_devices_path.return_value = self.temp_dir
522+ mocked_get_device_file.return_value = None
523+ mocked_str.return_value = ""
524
525 add_command = add(self.parser, self.args)
526- add_command.edit_file = MagicMock()
527 add_command.invoke()
528
529 expected_path = os.path.join(self.temp_dir,
530 ".".join([self.device, "conf"]))
531 self.assertTrue(os.path.isfile(expected_path))
532
533+ @patch("lava.device.commands.edit_file", create=True)
534 @patch("lava.device.commands.get_known_device")
535 @patch("lava.device.commands.get_devices_path")
536 @patch("lava.device.commands.sys.exit")
537 @patch("lava.device.commands.get_device_file")
538 def test_add_invoke_1(self, mocked_get_device_file, mocked_sys_exit,
539- mocked_get_devices_path, mocked_get_known_device):
540+ mocked_get_devices_path, mocked_get_known_device,
541+ mocked_edit_file):
542 mocked_get_devices_path.return_value = self.temp_dir
543 mocked_get_device_file.return_value = self.temp_file.name
544
545 add_command = add(self.parser, self.args)
546- add_command.edit_file = MagicMock()
547 add_command.invoke()
548
549 self.assertTrue(mocked_sys_exit.called)
550@@ -97,11 +101,13 @@
551 name, args, kwargs = self.parser.method_calls[1]
552 self.assertIn("DEVICE", args)
553
554- @patch("lava.device.Device.__str__", new=MagicMock(return_value=""))
555- @patch("lava.device.Device.update", new=MagicMock())
556+ @patch("lava.device.commands.edit_file", create=True)
557+ @patch("lava.device.Device.__str__", return_value="")
558+ @patch("lava.device.Device.update")
559 @patch("lava.device.commands.get_device_file")
560 @patch("lava.device.commands.get_devices_path")
561- def test_remove_invoke(self, get_devices_path_mock, get_device_file_mock):
562+ def test_remove_invoke(self, get_devices_path_mock, get_device_file_mock,
563+ mocked_update, mocked_str, mocked_edit_file):
564 # Tests invocation of the remove command. Verifies that the conf file
565 # has been correctly removed.
566 # First we add a new conf file, then we remove it.
567@@ -109,7 +115,6 @@
568 get_devices_path_mock.return_value = self.temp_dir
569
570 add_command = add(self.parser, self.args)
571- add_command.edit_file = MagicMock()
572 add_command.invoke()
573
574 expected_path = os.path.join(self.temp_dir,
575@@ -145,18 +150,20 @@
576 name, args, kwargs = self.parser.method_calls[1]
577 self.assertIn("DEVICE", args)
578
579+ @patch("lava.device.commands.can_edit_file", create=True)
580+ @patch("lava.device.commands.edit_file", create=True)
581 @patch("lava.device.commands.get_device_file")
582- def test_config_invoke_0(self, mocked_get_device_file):
583+ def test_config_invoke_0(self, mocked_get_device_file, mocked_edit_file,
584+ mocked_can_edit_file):
585 command = config(self.parser, self.args)
586
587+ mocked_can_edit_file.return_value = True
588 mocked_get_device_file.return_value = self.temp_file.name
589- command.can_edit_file = MagicMock(return_value=True)
590- command.edit_file = MagicMock()
591 command.invoke()
592
593- self.assertTrue(command.edit_file.called)
594+ self.assertTrue(mocked_edit_file.called)
595 self.assertEqual([call(self.temp_file.name)],
596- command.edit_file.call_args_list)
597+ mocked_edit_file.call_args_list)
598
599 @patch("lava.device.commands.get_device_file",
600 new=MagicMock(return_value=None))
601
602=== modified file 'lava/device/tests/test_device.py'
603--- lava/device/tests/test_device.py 2013-07-25 15:43:25 +0000
604+++ lava/device/tests/test_device.py 2013-07-25 15:43:26 +0000
605@@ -20,19 +20,20 @@
606 Device class unit tests.
607 """
608
609-from lava.parameter import Parameter
610+from mock import patch
611+
612+from lava.config import Config
613+from lava.device import (
614+ Device,
615+ get_known_device,
616+)
617 from lava.device.templates import (
618 HOSTNAME_PARAMETER,
619 PANDA_DEVICE_TYPE,
620 PANDA_CONNECTION_COMMAND,
621 )
622-from lava.tests.test_config import MockedConfig
623-from lava.device import (
624- Device,
625- get_known_device,
626-)
627-from lava.tool.errors import CommandError
628 from lava.helper.tests.helper_test import HelperTest
629+from lava.parameter import Parameter
630
631
632 class DeviceTest(HelperTest):
633@@ -64,12 +65,14 @@
634 self.assertIsInstance(instance.data['device_type'], Parameter)
635 self.assertEqual(instance.data['device_type'].value, 'vexpress')
636
637- def test_device_update_1(self):
638+ @patch("lava.config.Config.save")
639+ def test_device_update_1(self, patched_save):
640 # Tests that when calling update() on a Device, the template gets
641 # updated with the correct values from a Config instance.
642 hostname = "panda_device"
643
644- config = MockedConfig(self.temp_file.name)
645+ config = Config()
646+ config._config_file = self.temp_file.name
647 config.put_parameter(HOSTNAME_PARAMETER, hostname)
648 config.put_parameter(PANDA_DEVICE_TYPE, "panda")
649 config.put_parameter(PANDA_CONNECTION_COMMAND, "test")
650@@ -85,12 +88,14 @@
651
652 self.assertEqual(expected, instance.data)
653
654- def test_device_write(self):
655+ @patch("lava.config.Config.save")
656+ def test_device_write(self, mocked_save):
657 # User tries to create a new panda device. The conf file is written
658 # and contains the expected results.
659 hostname = "panda_device"
660
661- config = MockedConfig(self.temp_file.name)
662+ config = Config()
663+ config._config_file = self.temp_file.name
664 config.put_parameter(HOSTNAME_PARAMETER, hostname)
665 config.put_parameter(PANDA_DEVICE_TYPE, "panda")
666 config.put_parameter(PANDA_CONNECTION_COMMAND, "test")
667
668=== modified file 'lava/helper/command.py'
669--- lava/helper/command.py 2013-07-25 15:43:25 +0000
670+++ lava/helper/command.py 2013-07-25 15:43:26 +0000
671@@ -19,22 +19,49 @@
672 """Base command class common to lava commands series."""
673
674 import os
675-import subprocess
676 import sys
677-
678+import xmlrpclib
679
680 from lava.config import InteractiveConfig
681+from lava.helper.dispatcher import get_devices
682+from lava.parameter import (
683+ Parameter,
684+ SingleChoiceParameter,
685+)
686 from lava.tool.command import Command
687 from lava.tool.errors import CommandError
688-from lava_tool.utils import has_command
689+from lava_tool.authtoken import (
690+ AuthenticatingServerProxy,
691+ KeyringAuthBackend
692+)
693+from lava_tool.utils import (
694+ has_command,
695+ verify_and_create_url,
696+ create_tar,
697+ base64_encode,
698+)
699+from lava.job import Job
700+from lava.job.templates import (
701+ LAVA_TEST_SHELL_TAR_REPO,
702+ LAVA_TEST_SHELL_TAR_REPO_KEY,
703+ LAVA_TEST_SHELL_TESDEF_KEY,
704+)
705+
706+from lava.testdef import TestDefinition
707+from lava.testdef.templates import (
708+ TESTDEF_TEMPLATE,
709+)
710+CONFIG = InteractiveConfig()
711
712
713 class BaseCommand(Command):
714+
715 """Base command class for all lava commands."""
716+
717 def __init__(self, parser, args):
718 super(BaseCommand, self).__init__(parser, args)
719- self.config = InteractiveConfig(
720- force_interactive=self.args.non_interactive)
721+ self.config = CONFIG
722+ self.config.force_interactive = self.args.non_interactive
723
724 @classmethod
725 def register_arguments(cls, parser):
726@@ -43,59 +70,109 @@
727 action='store_false',
728 help=("Do not ask for input parameters."))
729
730- @classmethod
731- def can_edit_file(cls, conf_file):
732- """Checks if a file can be opend in write mode.
733-
734- :param conf_file: The path to the file.
735- :return True if it is possible to write on the file, False otherwise.
736- """
737- can_edit = True
738- try:
739- fp = open(conf_file, "a")
740- fp.close()
741- except IOError:
742- can_edit = False
743- return can_edit
744-
745- @classmethod
746- def edit_file(cls, config_file):
747- """Opens the specified file with the default file editor.
748-
749- :param config_file: The file to edit.
750- """
751- editor = os.environ.get("EDITOR", None)
752- if editor is None:
753- if has_command("sensible-editor"):
754- editor = "sensible-editor"
755- elif has_command("xdg-open"):
756- editor = "xdg-open"
757+ def authenticated_server(self):
758+ """Returns a connection to a LAVA server.
759+
760+ It will ask the user the necessary parameters to establish the
761+ connection.
762+ """
763+ server_name_parameter = Parameter("server")
764+ rpc_endpoint_parameter = Parameter("rpc_endpoint",
765+ depends=server_name_parameter)
766+
767+ server_url = self.config.get(server_name_parameter)
768+ endpoint = self.config.get(rpc_endpoint_parameter)
769+
770+ rpc_url = verify_and_create_url(server_url, endpoint)
771+ server = AuthenticatingServerProxy(rpc_url,
772+ auth_backend=KeyringAuthBackend())
773+ return server
774+
775+ def submit(self, job_file):
776+ """Submits a job file to a LAVA server.
777+
778+ :param job_file: The job file to submit.
779+ :return The job ID on success.
780+ """
781+ if os.path.isfile(job_file):
782+ try:
783+ jobdata = open(job_file, 'rb').read()
784+ server = self.authenticated_server()
785+
786+ job_id = server.scheduler.submit_job(jobdata)
787+ print >> sys.stdout, ("Job submitted with job "
788+ "ID {0}.".format(job_id))
789+
790+ return job_id
791+ except xmlrpclib.Fault, exc:
792+ raise CommandError(str(exc))
793+ else:
794+ raise CommandError("Job file '{0}' does not exists, or is not "
795+ "a file.".format(job_file))
796+
797+ def run(self, job_file):
798+ """Runs a job file on the local LAVA dispatcher.
799+
800+ :param job_file: The job file to run.
801+ """
802+ if os.path.isfile(job_file):
803+ if has_command("lava-dispatch"):
804+ devices = get_devices()
805+ if devices:
806+ if len(devices) > 1:
807+ device_names = [device.hostname for device in devices]
808+ device_param = SingleChoiceParameter("device",
809+ device_names)
810+ device = device_param.prompt("Device to use: ")
811+ else:
812+ device = devices[0].hostname
813+ self.execute(
814+ ["lava-dispatch", "--target", device, job_file])
815 else:
816- # We really do not know how to open a file.
817- print >> sys.stdout, ("Cannot find an editor to open the "
818- "file '{0}'.".format(config_file))
819- print >> sys.stdout, ("Either set the 'EDITOR' environment "
820- "variable, or install 'sensible-editor' "
821- "or 'xdg-open'.")
822- sys.exit(-1)
823- try:
824- subprocess.Popen([editor, config_file]).wait()
825- except Exception:
826- raise CommandError("Error opening the file '{0}' with the "
827- "following editor: {1}.".format(config_file,
828- editor))
829-
830- @classmethod
831- def run(cls, cmd_args):
832- """Runs the supplied command args.
833-
834- :param cmd_args: The command, and its optional arguments, to run.
835- :return The command execution return code.
836- """
837- if not isinstance(cmd_args, list):
838- cmd_args = [cmd_args]
839- try:
840- return subprocess.check_call(cmd_args)
841- except subprocess.CalledProcessError:
842- raise CommandError("Error running the following command: "
843- "{0}".format(" ".join(cmd_args)))
844+ raise CommandError("Cannot find lava-dispatcher installation.")
845+ else:
846+ raise CommandError("Job file '{0}' does not exists, or it is not "
847+ "a file.".format(job_file))
848+
849+ def create_tar_repo_job(self, job_file, testdef_file, tar_content):
850+ """Creates a job file based on the tar-repo template.
851+
852+ The tar repo is not kept on the file system.
853+
854+ :param job_file: The path of the job file to create.
855+ :param testdef_file: The path of the test definition file.
856+ :param tar_content: What should go into the tarball repository.
857+ :return The path of the job file created.
858+ """
859+ try:
860+ tar_repo = create_tar(tar_content)
861+
862+ job_instance = Job(LAVA_TEST_SHELL_TAR_REPO, job_file)
863+ job_instance.update(self.config)
864+
865+ job_instance.set(LAVA_TEST_SHELL_TAR_REPO_KEY,
866+ base64_encode(tar_repo))
867+ job_instance.set(LAVA_TEST_SHELL_TESDEF_KEY,
868+ os.path.basename(testdef_file))
869+
870+ job_instance.write()
871+
872+ return job_instance.file_name
873+ finally:
874+ if os.path.isfile(tar_repo):
875+ os.unlink(tar_repo)
876+
877+ def create_test_definition(self, testdef_file, template=TESTDEF_TEMPLATE):
878+ """Creates a test definition YAML file.
879+
880+ :param testdef_file: The file to create.
881+ :return The path of the file created.
882+ """
883+ testdef = TestDefinition(template, testdef_file)
884+ testdef.update(self.config)
885+ testdef.write()
886+
887+ print >> sys.stdout, ("Create test definition "
888+ "'{0}'.".format(testdef.file_name))
889+
890+ return testdef.file_name
891
892=== modified file 'lava/helper/template.py'
893--- lava/helper/template.py 2013-07-25 15:43:25 +0000
894+++ lava/helper/template.py 2013-07-25 15:43:26 +0000
895@@ -16,6 +16,8 @@
896 # You should have received a copy of the GNU Lesser General Public License
897 # along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
898
899+"""Helper functions for a template."""
900+
901 from lava.parameter import Parameter
902
903
904@@ -42,3 +44,81 @@
905 update(entry)
906
907 update(template)
908+
909+
910+def get_key(data, search_key):
911+ """Goes through a template looking for a key.
912+
913+ :param data: The template to traverse.
914+ :param search_key: The key to look for.
915+ :return The key value.
916+ """
917+ return_value = None
918+ found = False
919+
920+ if isinstance(data, dict):
921+ bucket = []
922+
923+ for key, value in data.iteritems():
924+ if key == search_key:
925+ return_value = value
926+ found = True
927+ break
928+ else:
929+ bucket.append(value)
930+
931+ if bucket and not found:
932+ for element in bucket:
933+ if isinstance(element, list):
934+ for element in element:
935+ bucket.append(element)
936+ elif isinstance(element, dict):
937+ for key, value in element.iteritems():
938+ if key == search_key:
939+ return_value = value
940+ found = True
941+ break
942+ else:
943+ bucket.append(value)
944+ if found:
945+ break
946+
947+ return return_value
948+
949+
950+def set_value(data, search_key, new_value):
951+ """Sets a new value for a template key.
952+
953+ :param data: The data structure to update.
954+ :type dict
955+ :param search_key: The key to search and update.
956+ :param new_value: The new value to set.
957+ """
958+ is_set = False
959+
960+ if isinstance(data, dict):
961+ bucket = []
962+
963+ for key, value in data.iteritems():
964+ if key == search_key:
965+ data[key] = new_value
966+ is_set = True
967+ break
968+ else:
969+ bucket.append(value)
970+
971+ if bucket and not is_set:
972+ for element in bucket:
973+ if isinstance(element, list):
974+ for element in element:
975+ bucket.append(element)
976+ elif isinstance(element, dict):
977+ for key, value in element.iteritems():
978+ if key == search_key:
979+ element[key] = new_value
980+ is_set = True
981+ break
982+ else:
983+ bucket.append(value)
984+ if is_set:
985+ break
986
987=== modified file 'lava/helper/tests/helper_test.py'
988--- lava/helper/tests/helper_test.py 2013-07-25 15:43:25 +0000
989+++ lava/helper/tests/helper_test.py 2013-07-25 15:43:26 +0000
990@@ -29,12 +29,20 @@
991 import tempfile
992
993 from unittest import TestCase
994-from mock import MagicMock
995+from mock import (
996+ MagicMock,
997+ patch
998+)
999
1000
1001 class HelperTest(TestCase):
1002 """Helper test class that all tests under the lava package can inherit."""
1003+
1004 def setUp(self):
1005+ # Need to patch it here, not as a decorator, or running the tests
1006+ # via `./setup.py test` will fail.
1007+ self.at_exit_patcher = patch("lava.config.AT_EXIT_CALLS", spec=set)
1008+ self.at_exit_patcher.start()
1009 self.original_stdout = sys.stdout
1010 sys.stdout = open("/dev/null", "w")
1011 self.original_stderr = sys.stderr
1012@@ -50,12 +58,24 @@
1013 self.args.interactive = MagicMock(return_value=False)
1014 self.args.DEVICE = self.device
1015
1016- self.config = MagicMock()
1017- self.config.get = MagicMock(return_value=self.temp_dir)
1018-
1019 def tearDown(self):
1020+ self.at_exit_patcher.stop()
1021 sys.stdin = self.original_stdin
1022 sys.stdout = self.original_stdout
1023 sys.stderr = self.original_stderr
1024 shutil.rmtree(self.temp_dir)
1025 os.unlink(self.temp_file.name)
1026+
1027+ def tmp(self, name):
1028+ """
1029+ Returns the full path to a file, or directory, called `name` in a
1030+ temporary directory.
1031+
1032+ This method does not create the file, it only gives a full filename
1033+ where you can actually write some data. The file will not be removed
1034+ by this method.
1035+
1036+ :param name: The name the file/directory should have.
1037+ :return A path.
1038+ """
1039+ return os.path.join(tempfile.gettempdir(), name)
1040
1041=== modified file 'lava/helper/tests/test_command.py'
1042--- lava/helper/tests/test_command.py 2013-07-25 15:43:25 +0000
1043+++ lava/helper/tests/test_command.py 2013-07-25 15:43:26 +0000
1044@@ -18,14 +18,7 @@
1045
1046 """lava.herlp.command module tests."""
1047
1048-import subprocess
1049-from mock import (
1050- MagicMock,
1051- call,
1052- patch,
1053-)
1054
1055-from lava.tool.errors import CommandError
1056 from lava.helper.command import BaseCommand
1057 from lava.helper.tests.helper_test import HelperTest
1058
1059@@ -39,85 +32,3 @@
1060 command.register_arguments(self.parser)
1061 name, args, kwargs = self.parser.method_calls[0]
1062 self.assertIn("--non-interactive", args)
1063-
1064- def test_can_edit_file(self):
1065- # Tests the can_edit_file method of the config command.
1066- # This is to make sure the device config file is not erased when
1067- # checking if it is possible to open it.
1068- expected = ("hostname = a_fake_panda02\nconnection_command = \n"
1069- "device_type = panda\n")
1070-
1071- command = BaseCommand(self.parser, self.args)
1072- conf_file = self.temp_file
1073-
1074- with open(conf_file.name, "w") as f:
1075- f.write(expected)
1076-
1077- self.assertTrue(command.can_edit_file(conf_file.name))
1078- obtained = ""
1079- with open(conf_file.name) as f:
1080- obtained = f.read()
1081-
1082- self.assertEqual(expected, obtained)
1083-
1084- @patch("lava.helper.command.subprocess")
1085- def test_run_0(self, mocked_subprocess):
1086- mocked_subprocess.check_call = MagicMock()
1087- BaseCommand.run("foo")
1088- self.assertEqual(mocked_subprocess.check_call.call_args_list,
1089- [call(["foo"])])
1090- self.assertTrue(mocked_subprocess.check_call.called)
1091-
1092- @patch("lava.helper.command.subprocess.check_call")
1093- def test_run_1(self, mocked_check_call):
1094- mocked_check_call.side_effect = subprocess.CalledProcessError(1, "foo")
1095- self.assertRaises(CommandError, BaseCommand.run, ["foo"])
1096-
1097- @patch("lava.helper.command.subprocess")
1098- @patch("lava.helper.command.has_command", return_value=False)
1099- @patch("lava.helper.command.os.environ.get", return_value=None)
1100- @patch("lava.helper.command.sys.exit")
1101- def test_edit_file_0(self, mocked_sys_exit, mocked_env_get,
1102- mocked_has_command, mocked_subprocess):
1103- BaseCommand.edit_file(self.temp_file.name)
1104- self.assertTrue(mocked_sys_exit.called)
1105-
1106- @patch("lava.helper.command.subprocess")
1107- @patch("lava.helper.command.has_command", side_effect=[True, False])
1108- @patch("lava.helper.command.os.environ.get", return_value=None)
1109- def test_edit_file_1(self, mocked_env_get, mocked_has_command,
1110- mocked_subprocess):
1111- mocked_subprocess.Popen = MagicMock()
1112- BaseCommand.edit_file(self.temp_file.name)
1113- expected = [call(["sensible-editor", self.temp_file.name])]
1114- self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
1115-
1116- @patch("lava.helper.command.subprocess")
1117- @patch("lava.helper.command.has_command", side_effect=[False, True])
1118- @patch("lava.helper.command.os.environ.get", return_value=None)
1119- def test_edit_file_2(self, mocked_env_get, mocked_has_command,
1120- mocked_subprocess):
1121- mocked_subprocess.Popen = MagicMock()
1122- BaseCommand.edit_file(self.temp_file.name)
1123- expected = [call(["xdg-open", self.temp_file.name])]
1124- self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
1125-
1126- @patch("lava.helper.command.subprocess")
1127- @patch("lava.helper.command.has_command", return_value=False)
1128- @patch("lava.helper.command.os.environ.get", return_value="vim")
1129- def test_edit_file_3(self, mocked_env_get, mocked_has_command,
1130- mocked_subprocess):
1131- mocked_subprocess.Popen = MagicMock()
1132- BaseCommand.edit_file(self.temp_file.name)
1133- expected = [call(["vim", self.temp_file.name])]
1134- self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
1135-
1136- @patch("lava.helper.command.subprocess")
1137- @patch("lava.helper.command.has_command", return_value=False)
1138- @patch("lava.helper.command.os.environ.get", return_value="vim")
1139- def test_edit_file_4(self, mocked_env_get, mocked_has_command,
1140- mocked_subprocess):
1141- mocked_subprocess.Popen = MagicMock()
1142- mocked_subprocess.Popen.side_effect = Exception()
1143- self.assertRaises(CommandError, BaseCommand.edit_file,
1144- self.temp_file.name)
1145
1146=== added file 'lava/helper/tests/test_template.py'
1147--- lava/helper/tests/test_template.py 1970-01-01 00:00:00 +0000
1148+++ lava/helper/tests/test_template.py 2013-07-25 15:43:26 +0000
1149@@ -0,0 +1,102 @@
1150+# Copyright (C) 2013 Linaro Limited
1151+#
1152+# Author: Milo Casagrande <milo.casagrande@linaro.org>
1153+#
1154+# This file is part of lava-tool.
1155+#
1156+# lava-tool is free software: you can redistribute it and/or modify
1157+# it under the terms of the GNU Lesser General Public License version 3
1158+# as published by the Free Software Foundation
1159+#
1160+# lava-tool is distributed in the hope that it will be useful,
1161+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1162+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1163+# GNU General Public License for more details.
1164+#
1165+# You should have received a copy of the GNU Lesser General Public License
1166+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
1167+
1168+""" """
1169+
1170+import copy
1171+from unittest import TestCase
1172+
1173+from lava.helper.template import (
1174+ get_key,
1175+ set_value
1176+)
1177+
1178+
1179+TEST_TEMPLATE = {
1180+ "key1": "value1",
1181+ "key2": [
1182+ "value2", "value3"
1183+ ],
1184+ "key3": [
1185+ {
1186+ "key4": "value4",
1187+ "key5": "value5"
1188+ },
1189+ {
1190+ "key6": "value6",
1191+ "key7": "value7"
1192+ },
1193+ [
1194+ {
1195+ "key8": "value8"
1196+ }
1197+ ]
1198+ ],
1199+ "key10": {
1200+ "key11": "value11"
1201+ }
1202+}
1203+
1204+
1205+class TestParameter(TestCase):
1206+
1207+ def test_get_key_simple_key(self):
1208+ expected = "value1"
1209+ obtained = get_key(TEST_TEMPLATE, "key1")
1210+ self.assertEquals(expected, obtained)
1211+
1212+ def test_get_key_nested_key(self):
1213+ expected = "value4"
1214+ obtained = get_key(TEST_TEMPLATE, "key4")
1215+ self.assertEquals(expected, obtained)
1216+
1217+ def test_get_key_nested_key_1(self):
1218+ expected = "value7"
1219+ obtained = get_key(TEST_TEMPLATE, "key7")
1220+ self.assertEquals(expected, obtained)
1221+
1222+ def test_get_key_nested_key_2(self):
1223+ expected = "value8"
1224+ obtained = get_key(TEST_TEMPLATE, "key8")
1225+ self.assertEquals(expected, obtained)
1226+
1227+ def test_get_key_nested_key_3(self):
1228+ expected = "value11"
1229+ obtained = get_key(TEST_TEMPLATE, "key11")
1230+ self.assertEquals(expected, obtained)
1231+
1232+ def test_set_value_0(self):
1233+ data = copy.deepcopy(TEST_TEMPLATE)
1234+ expected = "foo"
1235+ set_value(data, "key1", expected)
1236+ obtained = get_key(data, "key1")
1237+ self.assertEquals(expected, obtained)
1238+
1239+ def test_set_value_1(self):
1240+ data = copy.deepcopy(TEST_TEMPLATE)
1241+ expected = "foo"
1242+ set_value(data, "key6", expected)
1243+ obtained = get_key(data, "key6")
1244+ self.assertEquals(expected, obtained)
1245+
1246+ def test_set_value_2(self):
1247+ data = copy.deepcopy(TEST_TEMPLATE)
1248+ expected = "foo"
1249+ set_value(data, "key11", expected)
1250+ obtained = get_key(data, "key11")
1251+ self.assertEquals(expected, obtained)
1252
1253=== modified file 'lava/job/__init__.py'
1254--- lava/job/__init__.py 2013-07-25 15:43:25 +0000
1255+++ lava/job/__init__.py 2013-07-25 15:43:26 +0000
1256@@ -16,18 +16,46 @@
1257 # You should have received a copy of the GNU Lesser General Public License
1258 # along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
1259
1260+import json
1261+
1262 from copy import deepcopy
1263-import json
1264-
1265-from lava.helper.template import expand_template
1266-
1267-
1268-class Job:
1269- def __init__(self, template):
1270- self.data = deepcopy(template)
1271-
1272- def fill_in(self, config):
1273+
1274+from lava.helper.template import (
1275+ expand_template,
1276+ set_value,
1277+)
1278+from lava_tool.utils import (
1279+ verify_file_extension,
1280+ verify_path_existance,
1281+ write_file
1282+)
1283+
1284+# Default job file extension.
1285+DEFAULT_JOB_EXTENSION = "json"
1286+# Possible extension for a job file.
1287+JOB_FILE_EXTENSIONS = [DEFAULT_JOB_EXTENSION]
1288+
1289+
1290+class Job(object):
1291+ def __init__(self, data, file_name):
1292+ self.file_name = verify_file_extension(file_name,
1293+ DEFAULT_JOB_EXTENSION,
1294+ JOB_FILE_EXTENSIONS)
1295+ verify_path_existance(self.file_name)
1296+ self.data = deepcopy(data)
1297+
1298+ def set(self, key, value):
1299+ """Set key to the specified value.
1300+
1301+ :param key: The key to look in the object data.
1302+ :param value: The value to set.
1303+ """
1304+ set_value(self.data, key, value)
1305+
1306+ def update(self, config):
1307+ """Updates the Job object based on the provided config."""
1308 expand_template(self.data, config)
1309
1310- def write(self, stream):
1311- stream.write(json.dumps(self.data, indent=4))
1312+ def write(self):
1313+ """Writes the Job object to file."""
1314+ write_file(self.file_name, json.dumps(self.data, indent=4))
1315
1316=== modified file 'lava/job/commands.py'
1317--- lava/job/commands.py 2013-07-25 15:43:25 +0000
1318+++ lava/job/commands.py 2013-07-25 15:43:26 +0000
1319@@ -21,21 +21,14 @@
1320 """
1321
1322 import os
1323-import sys
1324-import xmlrpclib
1325
1326 from lava.helper.command import BaseCommand
1327-from lava.helper.dispatcher import get_devices
1328-
1329 from lava.job import Job
1330 from lava.job.templates import (
1331- BOOT_TEST,
1332+ BOOT_TEST_KEY,
1333+ JOB_TYPES,
1334 )
1335-from lava.parameter import Parameter
1336 from lava.tool.command import CommandGroup
1337-from lava.tool.errors import CommandError
1338-from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend
1339-from lava_tool.utils import has_command
1340
1341
1342 class job(CommandGroup):
1343@@ -50,18 +43,25 @@
1344 def register_arguments(cls, parser):
1345 super(new, cls).register_arguments(parser)
1346 parser.add_argument("FILE", help=("Job file to be created."))
1347-
1348- def invoke(self):
1349- if os.path.exists(self.args.FILE):
1350- raise CommandError('{0} already exists.'.format(self.args.FILE))
1351-
1352- with open(self.args.FILE, 'w') as job_file:
1353- job_instance = Job(BOOT_TEST)
1354- job_instance.fill_in(self.config)
1355- job_instance.write(job_file)
1356+ parser.add_argument("--type",
1357+ help=("The type of job to create. Defaults to "
1358+ "'{0}'.".format(BOOT_TEST_KEY)),
1359+ choices=JOB_TYPES.keys(),
1360+ default=BOOT_TEST_KEY)
1361+
1362+ def invoke(self, job_template=None):
1363+ if not job_template:
1364+ job_template = JOB_TYPES.get(self.args.type)
1365+
1366+ full_path = os.path.abspath(self.args.FILE)
1367+
1368+ job_instance = Job(job_template, full_path)
1369+ job_instance.update(self.config)
1370+ job_instance.write()
1371
1372
1373 class submit(BaseCommand):
1374+
1375 """Submits the specified job file."""
1376
1377 @classmethod
1378@@ -70,24 +70,11 @@
1379 parser.add_argument("FILE", help=("The job file to submit."))
1380
1381 def invoke(self):
1382- jobfile = self.args.FILE
1383- jobdata = open(jobfile, 'rb').read()
1384-
1385- server_name = Parameter('server')
1386- rpc_endpoint = Parameter('rpc_endpoint', depends=server_name)
1387- self.config.get(server_name)
1388- endpoint = self.config.get(rpc_endpoint)
1389-
1390- server = AuthenticatingServerProxy(endpoint,
1391- auth_backend=KeyringAuthBackend())
1392- try:
1393- job_id = server.scheduler.submit_job(jobdata)
1394- print >> sys.stdout, "Job submitted with job ID {0}".format(job_id)
1395- except xmlrpclib.Fault, exc:
1396- raise CommandError(str(exc))
1397+ super(submit, self).submit(self.args.FILE)
1398
1399
1400 class run(BaseCommand):
1401+
1402 """Runs the specified job file on the local dispatcher."""
1403
1404 @classmethod
1405@@ -95,48 +82,5 @@
1406 super(run, cls).register_arguments(parser)
1407 parser.add_argument("FILE", help=("The job file to submit."))
1408
1409- @classmethod
1410- def _choose_device(cls, devices):
1411- """Let the user choose the device to use.
1412-
1413- :param devices: The list of available devices.
1414- :return The selected device.
1415- """
1416- devices_len = len(devices)
1417- output_list = []
1418- for device, number in zip(devices, range(1, devices_len + 1)):
1419- output_list.append("\t{0}. {1}\n".format(number, device.hostname))
1420-
1421- print >> sys.stdout, ("More than one local device found. "
1422- "Please choose one:\n")
1423- print >> sys.stdout, "".join(output_list)
1424-
1425- while True:
1426- try:
1427- user_input = raw_input("Device number to use: ").strip()
1428-
1429- if user_input in [str(x) for x in range(1, devices_len + 1)]:
1430- return devices[int(user_input) - 1].hostname
1431- else:
1432- continue
1433- except EOFError:
1434- user_input = None
1435- except KeyboardInterrupt:
1436- sys.exit(-1)
1437-
1438 def invoke(self):
1439- if os.path.isfile(self.args.FILE):
1440- if has_command("lava-dispatch"):
1441- devices = get_devices()
1442- if devices:
1443- if len(devices) > 1:
1444- device = self._choose_device(devices)
1445- else:
1446- device = devices[0].hostname
1447- self.run(["lava-dispatch", "--target", device,
1448- self.args.FILE])
1449- else:
1450- raise CommandError("Cannot find lava-dispatcher installation.")
1451- else:
1452- raise CommandError("The file '{0}' does not exists. or is not "
1453- "a file.".format(self.args.FILE))
1454+ super(run, self).run(self.args.FILE)
1455
1456=== modified file 'lava/job/templates.py'
1457--- lava/job/templates.py 2013-07-25 15:43:25 +0000
1458+++ lava/job/templates.py 2013-07-25 15:43:26 +0000
1459@@ -16,19 +16,33 @@
1460 # You should have received a copy of the GNU Lesser General Public License
1461 # along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
1462
1463-from lava.parameter import Parameter
1464-
1465-device_type = Parameter("device_type")
1466-prebuilt_image = Parameter("prebuilt_image", depends=device_type)
1467+from lava.parameter import (
1468+ ListParameter,
1469+ Parameter,
1470+)
1471+
1472+LAVA_TEST_SHELL_TAR_REPO_KEY = "tar-repo"
1473+LAVA_TEST_SHELL_TESDEF_KEY = "testdef"
1474+
1475+DEVICE_TYPE_PARAMETER = Parameter("device_type")
1476+PREBUILT_IMAGE_PARAMETER = Parameter("image", depends=DEVICE_TYPE_PARAMETER)
1477+
1478+TESTDEF_URLS_PARAMETER = ListParameter("testdef_urls")
1479+TESTDEF_URLS_PARAMETER.store = False
1480+
1481+# Use another ID for the server parameter, might be different.
1482+SERVER_PARAMETER = Parameter("stream_server")
1483+STREAM_PARAMETER = Parameter("stream")
1484
1485 BOOT_TEST = {
1486+ "timeout": 18000,
1487 "job_name": "Boot test",
1488- "device_type": device_type,
1489+ "device_type": DEVICE_TYPE_PARAMETER,
1490 "actions": [
1491 {
1492 "command": "deploy_linaro_image",
1493 "parameters": {
1494- "image": prebuilt_image
1495+ "image": PREBUILT_IMAGE_PARAMETER
1496 }
1497 },
1498 {
1499@@ -39,21 +53,72 @@
1500
1501 LAVA_TEST_SHELL = {
1502 "job_name": "LAVA Test Shell",
1503- "device_type": device_type,
1504- "actions": [
1505- {
1506- "command": "deploy_linaro_image",
1507- "parameters": {
1508- "image": prebuilt_image,
1509- }
1510- },
1511- {
1512- "command": "lava_test_shell",
1513- "parameters": {
1514- "testdef_urls": [
1515- Parameter("testdef_url")
1516+ "timeout": 18000,
1517+ "device_type": DEVICE_TYPE_PARAMETER,
1518+ "actions": [
1519+ {
1520+ "command": "deploy_linaro_image",
1521+ "parameters": {
1522+ "image": PREBUILT_IMAGE_PARAMETER,
1523+ }
1524+ },
1525+ {
1526+ "command": "lava_test_shell",
1527+ "parameters": {
1528+ "timeout": 1800,
1529+ "testdef_urls": TESTDEF_URLS_PARAMETER,
1530+ }
1531+ },
1532+ {
1533+ "command": "submit_results",
1534+ "parameters" : {
1535+ "stream": STREAM_PARAMETER,
1536+ "server": SERVER_PARAMETER
1537+ }
1538+ }
1539+ ]
1540+}
1541+
1542+# This is a special case template, only use when automatically create job files
1543+# starting from a testdef or a script. Never to be used directly by the user.
1544+LAVA_TEST_SHELL_TAR_REPO = {
1545+ "job_name": "LAVA Test Shell",
1546+ "timeout": 18000,
1547+ "device_type": DEVICE_TYPE_PARAMETER,
1548+ "actions": [
1549+ {
1550+ "command": "deploy_linaro_image",
1551+ "parameters": {
1552+ "image": PREBUILT_IMAGE_PARAMETER,
1553+ }
1554+ },
1555+ {
1556+ "command": "lava_test_shell",
1557+ "parameters": {
1558+ "timeout": 1800,
1559+ "testdef_repos": [
1560+ {
1561+ LAVA_TEST_SHELL_TESDEF_KEY: None,
1562+ LAVA_TEST_SHELL_TAR_REPO_KEY: None,
1563+ }
1564 ]
1565 }
1566+ },
1567+ {
1568+ "command": "submit_results",
1569+ "parameters" : {
1570+ "stream": STREAM_PARAMETER,
1571+ "server": SERVER_PARAMETER
1572+ }
1573 }
1574 ]
1575 }
1576+
1577+BOOT_TEST_KEY = "boot-test"
1578+LAVA_TEST_SHELL_KEY = "lava-test-shell"
1579+
1580+# Dict with all the user available job templates.
1581+JOB_TYPES = {
1582+ BOOT_TEST_KEY: BOOT_TEST,
1583+ LAVA_TEST_SHELL_KEY: LAVA_TEST_SHELL,
1584+}
1585
1586=== modified file 'lava/job/tests/test_commands.py'
1587--- lava/job/tests/test_commands.py 2013-07-25 15:43:25 +0000
1588+++ lava/job/tests/test_commands.py 2013-07-25 15:43:26 +0000
1589@@ -41,6 +41,7 @@
1590 def setUp(self):
1591 super(CommandTest, self).setUp()
1592 self.args.FILE = self.temp_file.name
1593+ self.args.type = "boot-test"
1594
1595 self.device_type = Parameter('device_type')
1596 self.prebuilt_image = Parameter('prebuilt_image',
1597@@ -49,14 +50,6 @@
1598 self.config.put_parameter(self.device_type, 'foo')
1599 self.config.put_parameter(self.prebuilt_image, 'bar')
1600
1601- def tmp(self, filename):
1602- """Returns a path to a non existent file.
1603-
1604- :param filename: The name the file should have.
1605- :return A path.
1606- """
1607- return os.path.join(self.temp_dir, filename)
1608-
1609
1610 class JobNewTest(CommandTest):
1611
1612@@ -106,7 +99,7 @@
1613 command = run(self.parser, self.args)
1614 self.assertRaises(CommandError, command.invoke)
1615
1616- @patch("lava.job.commands.has_command", new=MagicMock(return_value=False))
1617+ @patch("lava_tool.utils.has_command", new=MagicMock(return_value=False))
1618 def test_invoke_raises_1(self):
1619 # Users passes a valid file to the run command, but she does not have
1620 # the dispatcher installed.
1621
1622=== modified file 'lava/job/tests/test_job.py'
1623--- lava/job/tests/test_job.py 2013-07-25 15:43:25 +0000
1624+++ lava/job/tests/test_job.py 2013-07-25 15:43:26 +0000
1625@@ -20,62 +20,73 @@
1626 Unit tests for the Job class
1627 """
1628
1629+import os
1630 import json
1631-import os
1632 import tempfile
1633
1634-from StringIO import StringIO
1635-from unittest import TestCase
1636+from mock import patch
1637
1638 from lava.config import Config
1639+from lava.helper.tests.helper_test import HelperTest
1640 from lava.job import Job
1641 from lava.job.templates import BOOT_TEST
1642 from lava.parameter import Parameter
1643
1644
1645-class JobTest(TestCase):
1646+class JobTest(HelperTest):
1647
1648- def setUp(self):
1649- self.config_file = tempfile.NamedTemporaryFile(delete=False)
1650+ @patch("lava.config.Config.save")
1651+ def setUp(self, mocked_config):
1652+ super(JobTest, self).setUp()
1653 self.config = Config()
1654- self.config._config_file = self.config_file.name
1655-
1656- def tearDown(self):
1657- if os.path.isfile(self.config_file.name):
1658- os.unlink(self.config_file.name)
1659+ self.config.config_file = self.temp_file.name
1660
1661 def test_from_template(self):
1662 template = {}
1663- job = Job(template)
1664+ job = Job(template, self.temp_file.name)
1665 self.assertEqual(job.data, template)
1666 self.assertIsNot(job.data, template)
1667
1668- def test_fill_in_data(self):
1669+ def test_update_data(self):
1670 image = "/path/to/panda.img"
1671 param1 = Parameter("device_type")
1672- param2 = Parameter("prebuilt_image", depends=param1)
1673+ param2 = Parameter("image", depends=param1)
1674 self.config.put_parameter(param1, "panda")
1675 self.config.put_parameter(param2, image)
1676
1677- job = Job(BOOT_TEST)
1678- job.fill_in(self.config)
1679+ job = Job(BOOT_TEST, self.temp_file.name)
1680+ job.update(self.config)
1681
1682 self.assertEqual(job.data['device_type'], "panda")
1683 self.assertEqual(job.data['actions'][0]["parameters"]["image"], image)
1684
1685 def test_write(self):
1686- orig_data = {"foo": "bar"}
1687- job = Job(orig_data)
1688- output = StringIO()
1689- job.write(output)
1690-
1691- data = json.loads(output.getvalue())
1692- self.assertEqual(data, orig_data)
1693+ try:
1694+ orig_data = {"foo": "bar"}
1695+ job_file = os.path.join(tempfile.gettempdir(), "a_json_file.json")
1696+ job = Job(orig_data, job_file)
1697+ job.write()
1698+
1699+ output = ""
1700+ with open(job_file) as read_file:
1701+ output = read_file.read()
1702+
1703+ data = json.loads(output)
1704+ self.assertEqual(data, orig_data)
1705+ finally:
1706+ os.unlink(job_file)
1707
1708 def test_writes_nicely_formatted_json(self):
1709- orig_data = {"foo": "bar"}
1710- job = Job(orig_data)
1711- output = StringIO()
1712- job.write(output)
1713-
1714- self.assertTrue(output.getvalue().startswith("{\n"))
1715+ try:
1716+ orig_data = {"foo": "bar"}
1717+ job_file = os.path.join(tempfile.gettempdir(), "b_json_file.json")
1718+ job = Job(orig_data, job_file)
1719+ job.write()
1720+
1721+ output = ""
1722+ with open(job_file) as read_file:
1723+ output = read_file.read()
1724+
1725+ self.assertTrue(output.startswith("{\n"))
1726+ finally:
1727+ os.unlink(job_file)
1728
1729=== modified file 'lava/parameter.py'
1730--- lava/parameter.py 2013-07-25 15:43:25 +0000
1731+++ lava/parameter.py 2013-07-25 15:43:26 +0000
1732@@ -20,7 +20,19 @@
1733 Parameter class and its accessory methods/functions.
1734 """
1735
1736+import StringIO
1737+import base64
1738+import os
1739 import sys
1740+import tarfile
1741+import tempfile
1742+import types
1743+
1744+from lava.tool.errors import CommandError
1745+from lava_tool.utils import to_list
1746+
1747+# Character used to join serialized list parameters.
1748+LIST_SERIALIZE_DELIMITER = ","
1749
1750
1751 class Parameter(object):
1752@@ -38,6 +50,15 @@
1753 self.value = value
1754 self.depends = depends
1755 self.asked = False
1756+ # Whether to store or not the parameter in the user config file.
1757+ self.store = True
1758+
1759+ def set(self, value):
1760+ """Sets the value of the parameter.
1761+
1762+ :param value: The value to set.
1763+ """
1764+ self.value = value
1765
1766 def prompt(self, old_value=None):
1767 """Gets the parameter value from the user.
1768@@ -50,26 +71,187 @@
1769 :param old_value: The old parameter value.
1770 :return The input as typed by the user, or the old value.
1771 """
1772- if old_value is not None:
1773- prompt = "{0} [{1}]: ".format(self.id, old_value)
1774- else:
1775- prompt = "{0}: ".format(self.id)
1776-
1777- user_input = None
1778+ if not self.asked:
1779+ if old_value is not None:
1780+ prompt = "{0} [{1}]: ".format(self.id, old_value)
1781+ else:
1782+ prompt = "{0}: ".format(self.id)
1783+
1784+ user_input = self.get_user_input(prompt)
1785+
1786+ if user_input is not None:
1787+ if len(user_input) == 0 and old_value:
1788+ # Keep the old value when user press enter or another
1789+ # whitespace char.
1790+ self.value = old_value
1791+ else:
1792+ self.value = user_input
1793+
1794+ self.asked = True
1795+
1796+ return self.value
1797+
1798+ @classmethod
1799+ def get_user_input(cls, prompt=""):
1800+ """Asks the user for input data.
1801+
1802+ :param prompt: The prompt that should be given to the user.
1803+ :return A string with what the user typed.
1804+ """
1805+ data = None
1806 try:
1807- user_input = raw_input(prompt).strip()
1808+ data = raw_input(prompt).strip()
1809 except EOFError:
1810- pass
1811+ # Force to return None.
1812+ data = None
1813 except KeyboardInterrupt:
1814 sys.exit(-1)
1815-
1816- if user_input is not None:
1817+ return data
1818+
1819+ @classmethod
1820+ def serialize(cls, value):
1821+ """Serializes the passed value to be friendly written to file.
1822+
1823+ Lists are serialized as a comma separated string of values.
1824+
1825+ :param value: The value to serialize.
1826+ :return The serialized value as string.
1827+ """
1828+ serialized = ""
1829+ if isinstance(value, list):
1830+ serialized = LIST_SERIALIZE_DELIMITER.join(
1831+ str(x) for x in value if x)
1832+ else:
1833+ serialized = str(value)
1834+ return serialized
1835+
1836+ @classmethod
1837+ def deserialize(cls, value):
1838+ """Deserialize a value into a list.
1839+
1840+ The value must have been serialized with the class instance serialize()
1841+ method.
1842+
1843+ :param value: The string value to be deserialized.
1844+ :type str
1845+ :return A list of values.
1846+ """
1847+ deserialized = []
1848+ if isinstance(value, types.StringTypes):
1849+ deserialized = filter(None, (x.strip() for x in value.split(
1850+ LIST_SERIALIZE_DELIMITER)))
1851+ else:
1852+ deserialized = list(value)
1853+ return deserialized
1854+
1855+
1856+class SingleChoiceParameter(Parameter):
1857+ """A parameter implemeting a single choice between multiple choices."""
1858+ def __init__(self, id, choices):
1859+ super(SingleChoiceParameter, self).__init__(id)
1860+ self.choices = to_list(choices)
1861+
1862+ def prompt(self, prompt, old_value=None):
1863+ """Asks the user for their choice."""
1864+ # Sliglty different than the other parameters: here we first present
1865+ # the user with what the choices are about.
1866+ print >> sys.stdout, prompt
1867+
1868+ index = 1
1869+ for choice in self.choices:
1870+ print >> sys.stdout, "\t{0:d}. {1}".format(index, choice)
1871+ index += 1
1872+
1873+ choices_len = len(self.choices)
1874+ while True:
1875+ user_input = self.get_user_input("Choice: ")
1876+
1877 if len(user_input) == 0 and old_value:
1878- # Keep the old value when user press enter or another
1879- # whitespace char.
1880- self.value = old_value
1881- else:
1882- self.value = user_input
1883-
1884- self.asked = True
1885+ choice = old_value
1886+ break
1887+ elif user_input in [str(x) for x in range(1, choices_len + 1)]:
1888+ choice = self.choices[int(user_input) - 1]
1889+ break
1890+
1891+ return choice
1892+
1893+
1894+class ListParameter(Parameter):
1895+ """A specialized Parameter to handle list values."""
1896+
1897+ # This is used as a deletion character. When we have an old value and the
1898+ # user enters this char, it sort of deletes the value.
1899+ DELETE_CHAR = "-"
1900+
1901+ def __init__(self, id, value=None, depends=None):
1902+ super(ListParameter, self).__init__(id, depends=depends)
1903+ self.value = []
1904+ if value:
1905+ self.set(value)
1906+
1907+ def set(self, value):
1908+ """Sets the value of the parameter.
1909+
1910+ :param value: The value to set.
1911+ """
1912+ self.value = to_list(value)
1913+
1914+ def add(self, value):
1915+ """Adds a new value to the list of values of this parameter.
1916+
1917+ :param value: The value to add.
1918+ """
1919+ if isinstance(value, list):
1920+ self.value.extend(value)
1921+ else:
1922+ self.value.append(value)
1923+
1924+ def prompt(self, old_value=None):
1925+ """Gets the parameter in a list form.
1926+
1927+ To exit the input procedure it is necessary to insert an empty line.
1928+
1929+ :return The list of values.
1930+ """
1931+
1932+ if not self.asked:
1933+ if old_value is not None:
1934+ # We might get the old value read from file via ConfigParser,
1935+ # and usually it comes in string format.
1936+ old_value = self.deserialize(old_value)
1937+
1938+ print >> sys.stdout, "Values for '{0}': ".format(self.id)
1939+
1940+ index = 1
1941+ while True:
1942+ user_input = None
1943+ if old_value is not None and (0 < len(old_value) >= index):
1944+ prompt = "{0:>3d}.\n\told: {1}\n\tnew: ".format(
1945+ index, old_value[index-1])
1946+ user_input = self.get_user_input(prompt)
1947+ else:
1948+ prompt = "{0:>3d}. ".format(index)
1949+ user_input = self.get_user_input(prompt)
1950+
1951+ if user_input is not None:
1952+ # The user has pressed Enter.
1953+ if len(user_input) == 0:
1954+ if old_value is not None and \
1955+ (0 < len(old_value) >= index):
1956+ user_input = old_value[index-1]
1957+ else:
1958+ break
1959+
1960+ if len(user_input) == 1 and user_input == \
1961+ self.DELETE_CHAR and (0 < len(old_value) >= index):
1962+ # We have an old value, user presses the DELETE_CHAR
1963+ # and we do not store anything. This is done to delete
1964+ # an old entry.
1965+ pass
1966+ else:
1967+ self.value.append(user_input)
1968+ index += 1
1969+
1970+ self.asked = True
1971+
1972 return self.value
1973
1974=== added directory 'lava/testdef'
1975=== added file 'lava/testdef/__init__.py'
1976--- lava/testdef/__init__.py 1970-01-01 00:00:00 +0000
1977+++ lava/testdef/__init__.py 2013-07-25 15:43:26 +0000
1978@@ -0,0 +1,60 @@
1979+# Copyright (C) 2013 Linaro Limited
1980+#
1981+# Author: Milo Casagrande <milo.casagrande@linaro.org>
1982+#
1983+# This file is part of lava-tool.
1984+#
1985+# lava-tool is free software: you can redistribute it and/or modify
1986+# it under the terms of the GNU Lesser General Public License version 3
1987+# as published by the Free Software Foundation
1988+#
1989+# lava-tool is distributed in the hope that it will be useful,
1990+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1991+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1992+# GNU General Public License for more details.
1993+#
1994+# You should have received a copy of the GNU Lesser General Public License
1995+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
1996+
1997+import yaml
1998+
1999+from copy import deepcopy
2000+
2001+from lava.helper.template import expand_template
2002+from lava_tool.utils import (
2003+ write_file,
2004+ verify_path_existance,
2005+ verify_file_extension
2006+)
2007+
2008+# Default test def file extension.
2009+DEFAULT_TESTDEF_EXTENSION = "yaml"
2010+# Possible extensions for a test def file.
2011+TESTDEF_FILE_EXTENSIONS = [DEFAULT_TESTDEF_EXTENSION]
2012+
2013+
2014+class TestDefinition(object):
2015+
2016+ def __init__(self, data, file_name):
2017+ """Initialize the object.
2018+
2019+ :param data: The serializable data to be used, usually a template.
2020+ :type dict
2021+ :param file_name: Where the test definition will be written.
2022+ :type str
2023+ """
2024+ self.file_name = verify_file_extension(file_name,
2025+ DEFAULT_TESTDEF_EXTENSION,
2026+ TESTDEF_FILE_EXTENSIONS)
2027+ verify_path_existance(self.file_name)
2028+
2029+ self.data = deepcopy(data)
2030+
2031+ def write(self):
2032+ """Writes the test definition to file."""
2033+ content = yaml.dump(self.data, default_flow_style=False, indent=4)
2034+ write_file(self.file_name, content)
2035+
2036+ def update(self, config):
2037+ """Updates the TestDefinition object based on the provided config."""
2038+ expand_template(self.data, config)
2039
2040=== added file 'lava/testdef/commands.py'
2041--- lava/testdef/commands.py 1970-01-01 00:00:00 +0000
2042+++ lava/testdef/commands.py 2013-07-25 15:43:26 +0000
2043@@ -0,0 +1,72 @@
2044+# Copyright (C) 2013 Linaro Limited
2045+#
2046+# Author: Milo Casagrande <milo.casagrande@linaro.org>
2047+#
2048+# This file is part of lava-tool.
2049+#
2050+# lava-tool is free software: you can redistribute it and/or modify
2051+# it under the terms of the GNU Lesser General Public License version 3
2052+# as published by the Free Software Foundation
2053+#
2054+# lava-tool is distributed in the hope that it will be useful,
2055+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2056+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2057+# GNU General Public License for more details.
2058+#
2059+# You should have received a copy of the GNU Lesser General Public License
2060+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
2061+
2062+"""
2063+Test definition commands class.
2064+"""
2065+
2066+import os
2067+
2068+from lava.helper.command import BaseCommand
2069+from lava.tool.command import CommandGroup
2070+
2071+
2072+class testdef(CommandGroup):
2073+
2074+ """LAVA test definitions handling."""
2075+
2076+ namespace = "lava.testdef.commands"
2077+
2078+
2079+class new(BaseCommand):
2080+
2081+ """Creates a new test definition file."""
2082+
2083+ @classmethod
2084+ def register_arguments(cls, parser):
2085+ super(new, cls).register_arguments(parser)
2086+ parser.add_argument("FILE", help="Test definition file to create.")
2087+
2088+ def invoke(self):
2089+ full_path = os.path.abspath(self.args.FILE)
2090+ self.create_test_definition(full_path)
2091+
2092+
2093+class run(BaseCommand):
2094+
2095+ """Runs the specified test definition on a local device."""
2096+
2097+ @classmethod
2098+ def register_arguments(cls, parser):
2099+ super(run, cls).register_arguments(parser)
2100+ parser.add_argument("FILE", help="Test definition file to run.")
2101+
2102+ def invoke(self):
2103+ pass
2104+
2105+
2106+def submit(BaseCommand):
2107+ """Submits the specified test definition to a remove LAVA server."""
2108+
2109+ @classmethod
2110+ def register_arguments(cls, parser):
2111+ super(submit, cls).register_arguments(parser)
2112+ parser.add_argument("FILE", help="Test definition file to send.")
2113+
2114+ def invoke(self):
2115+ pass
2116
2117=== added file 'lava/testdef/templates.py'
2118--- lava/testdef/templates.py 1970-01-01 00:00:00 +0000
2119+++ lava/testdef/templates.py 2013-07-25 15:43:26 +0000
2120@@ -0,0 +1,75 @@
2121+# Copyright (C) 2013 Linaro Limited
2122+#
2123+# Author: Milo Casagrande <milo.casagrande@linaro.org>
2124+#
2125+# This file is part of lava-tool.
2126+#
2127+# lava-tool is free software: you can redistribute it and/or modify
2128+# it under the terms of the GNU Lesser General Public License version 3
2129+# as published by the Free Software Foundation
2130+#
2131+# lava-tool is distributed in the hope that it will be useful,
2132+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2133+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2134+# GNU General Public License for more details.
2135+#
2136+# You should have received a copy of the GNU Lesser General Public License
2137+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
2138+
2139+"""Test definition templates."""
2140+
2141+from lava.parameter import (
2142+ ListParameter,
2143+ Parameter,
2144+)
2145+
2146+DEFAULT_TESTDEF_FILE = "lavatest.yaml"
2147+
2148+DEFAULT_TESTDEF_VERSION = "1.0"
2149+DEFAULT_TESTDEF_FORMAT = "Lava-Test Test Definition 1.0"
2150+
2151+# This is what will be called by default by the test definition yaml file.
2152+DEFAULT_TESTDEF_SCRIPT = "mytest.sh"
2153+DEFAULT_TESTDEF_SCRIPT_CONTENT = """#!/bin/sh
2154+# Automatic generated content by lava-tool.
2155+# Please add your own instructions.
2156+"""
2157+DEFAULT_TESTDEF_STEP = "./mytest.sh"
2158+
2159+DEFAULT_ENVIRONMET_VALUE = "lava_test_shell"
2160+
2161+# All these parameters will not be stored on the local config file.
2162+NAME_PARAMETER = Parameter("name")
2163+NAME_PARAMETER.store = False
2164+
2165+DESCRIPTION_PARAMETER = Parameter("description", depends=NAME_PARAMETER)
2166+DESCRIPTION_PARAMETER.store = False
2167+
2168+ENVIRONMENT_PARAMETER = ListParameter("environment",
2169+ depends=NAME_PARAMETER)
2170+ENVIRONMENT_PARAMETER.add(DEFAULT_ENVIRONMET_VALUE)
2171+ENVIRONMENT_PARAMETER.asked = True
2172+ENVIRONMENT_PARAMETER.store = False
2173+
2174+# Steps parameter. Default to a local shell script that the user defines.
2175+# We do not ask this parameter, and we do not store it either.
2176+STEPS_PARAMETER = ListParameter("steps", depends=NAME_PARAMETER)
2177+STEPS_PARAMETER.add(DEFAULT_TESTDEF_STEP)
2178+STEPS_PARAMETER.asked = True
2179+STEPS_PARAMETER.store = False
2180+
2181+TESTDEF_TEMPLATE = {
2182+ "metadata": {
2183+ "name": NAME_PARAMETER,
2184+ "format": DEFAULT_TESTDEF_FORMAT,
2185+ "version": DEFAULT_TESTDEF_VERSION,
2186+ "description": DESCRIPTION_PARAMETER,
2187+ "environment": ENVIRONMENT_PARAMETER,
2188+ },
2189+ "run": {
2190+ "steps": STEPS_PARAMETER,
2191+ },
2192+ "parse": {
2193+ "pattern": '^\s*(?P<test_case_id>\w+)=(?P<result>\w+)\s*$'
2194+ }
2195+}
2196
2197=== added directory 'lava/testdef/tests'
2198=== added file 'lava/testdef/tests/__init__.py'
2199=== added file 'lava/testdef/tests/test_commands.py'
2200--- lava/testdef/tests/test_commands.py 1970-01-01 00:00:00 +0000
2201+++ lava/testdef/tests/test_commands.py 2013-07-25 15:43:26 +0000
2202@@ -0,0 +1,153 @@
2203+# Copyright (C) 2013 Linaro Limited
2204+#
2205+# Author: Milo Casagrande <milo.casagrande@linaro.org>
2206+#
2207+# This file is part of lava-tool.
2208+#
2209+# lava-tool is free software: you can redistribute it and/or modify
2210+# it under the terms of the GNU Lesser General Public License version 3
2211+# as published by the Free Software Foundation
2212+#
2213+# lava-tool is distributed in the hope that it will be useful,
2214+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2215+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2216+# GNU General Public License for more details.
2217+#
2218+# You should have received a copy of the GNU Lesser General Public License
2219+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
2220+
2221+"""
2222+Tests for lava.testdef.commands.
2223+"""
2224+
2225+import os
2226+import tempfile
2227+import yaml
2228+
2229+from mock import (
2230+ patch,
2231+)
2232+
2233+from lava.config import InteractiveConfig
2234+from lava.helper.tests.helper_test import HelperTest
2235+from lava.testdef.commands import (
2236+ new,
2237+)
2238+from lava.tool.errors import CommandError
2239+
2240+
2241+class NewCommandTest(HelperTest):
2242+ """Class for the lava.testdef new command tests."""
2243+
2244+ @patch("lava.config.Config.save")
2245+ def setUp(self, mocked_save):
2246+ super(NewCommandTest, self).setUp()
2247+ self.file_name = "fake_testdef.yaml"
2248+ self.file_path = os.path.join(tempfile.gettempdir(), self.file_name)
2249+ self.args.FILE = self.file_path
2250+
2251+ self.temp_yaml = tempfile.NamedTemporaryFile(suffix=".yaml",
2252+ delete=False)
2253+
2254+ self.config_file = tempfile.NamedTemporaryFile(delete=False)
2255+ self.config = InteractiveConfig()
2256+ self.config.config_file = self.config_file.name
2257+ # Patch class raw_input, start it, and stop it on tearDown.
2258+ self.patcher1 = patch("lava.parameter.raw_input", create=True)
2259+ self.mocked_raw_input = self.patcher1.start()
2260+
2261+ def tearDown(self):
2262+ super(NewCommandTest, self).tearDown()
2263+ if os.path.isfile(self.file_path):
2264+ os.unlink(self.file_path)
2265+ os.unlink(self.config_file.name)
2266+ os.unlink(self.temp_yaml.name)
2267+ self.patcher1.stop()
2268+
2269+ def test_register_arguments(self):
2270+ # Make sure that the parser add_argument is called and we have the
2271+ # correct argument.
2272+ new_command = new(self.parser, self.args)
2273+ new_command.register_arguments(self.parser)
2274+
2275+ # Make sure we do not forget about this test.
2276+ self.assertEqual(2, len(self.parser.method_calls))
2277+
2278+ _, args, _ = self.parser.method_calls[0]
2279+ self.assertIn("--non-interactive", args)
2280+
2281+ _, args, _ = self.parser.method_calls[1]
2282+ self.assertIn("FILE", args)
2283+
2284+ def test_invoke_0(self):
2285+ # Test that passing a file on the command line, it is created on the
2286+ # file system.
2287+ self.mocked_raw_input.return_value = "\n"
2288+ new_command = new(self.parser, self.args)
2289+ new_command.invoke()
2290+ self.assertTrue(os.path.exists(self.file_path))
2291+
2292+ def test_invoke_1(self):
2293+ # Test that when passing an already existing file, an exception is
2294+ # thrown.
2295+ self.args.FILE = self.temp_yaml.name
2296+ new_command = new(self.parser, self.args)
2297+ self.assertRaises(CommandError, new_command.invoke)
2298+
2299+ def test_invoke_2(self):
2300+ # Tests that when adding a new test definition and writing it to file
2301+ # a correct YAML structure is created.
2302+ self.mocked_raw_input.return_value = "\n"
2303+ new_command = new(self.parser, self.args)
2304+ new_command.config = self.config
2305+ new_command.invoke()
2306+ expected = {'run': {'steps': ["./mytest.sh"]},
2307+ 'metadata': {
2308+ 'environment': ['lava_test_shell'],
2309+ 'format': 'Lava-Test Test Definition 1.0',
2310+ 'version': '1.0',
2311+ 'description': '',
2312+ 'name': ''},
2313+ 'parse': {
2314+ 'pattern':
2315+ '^\\s*(?P<test_case_id>\\w+)=(?P<result>\\w+)\\s*$'
2316+ },
2317+ }
2318+ obtained = None
2319+ with open(self.file_path, 'r') as read_file:
2320+ obtained = yaml.load(read_file)
2321+ self.assertEqual(expected, obtained)
2322+
2323+ def test_invoke_3(self):
2324+ # Tests that when adding a new test definition and writing it to a file
2325+ # in a directory withour permissions, exception is raised.
2326+ self.args.FILE = "/test_file.yaml"
2327+ self.mocked_raw_input.return_value = "\n"
2328+ new_command = new(self.parser, self.args)
2329+ self.assertRaises(CommandError, new_command.invoke)
2330+ self.assertFalse(os.path.exists(self.args.FILE))
2331+
2332+ def test_invoke_4(self):
2333+ # Tests that when passing values for the "steps" ListParameter, we get
2334+ # back the correct data structure.
2335+ self.mocked_raw_input.side_effect = ["foo", "\n", "\n", "\n", "\n",
2336+ "\n"]
2337+ new_command = new(self.parser, self.args)
2338+ new_command.invoke()
2339+ expected = {'run': {'steps': ["./mytest.sh"]},
2340+ 'metadata': {
2341+ 'environment': ['lava_test_shell'],
2342+ 'format': 'Lava-Test Test Definition 1.0',
2343+ 'version': '1.0',
2344+ 'description': '',
2345+ 'name': 'foo'
2346+ },
2347+ 'parse': {
2348+ 'pattern':
2349+ '^\\s*(?P<test_case_id>\\w+)=(?P<result>\\w+)\\s*$'
2350+ },
2351+ }
2352+ obtained = None
2353+ with open(self.file_path, 'r') as read_file:
2354+ obtained = yaml.load(read_file)
2355+ self.assertEqual(expected, obtained)
2356
2357=== added file 'lava/tests/test_commands.py'
2358--- lava/tests/test_commands.py 1970-01-01 00:00:00 +0000
2359+++ lava/tests/test_commands.py 2013-07-25 15:43:26 +0000
2360@@ -0,0 +1,128 @@
2361+# Copyright (C) 2013 Linaro Limited
2362+#
2363+# Author: Milo Casagrande <milo.casagrande@linaro.org>
2364+#
2365+# This file is part of lava-tool.
2366+#
2367+# lava-tool is free software: you can redistribute it and/or modify
2368+# it under the terms of the GNU Lesser General Public License version 3
2369+# as published by the Free Software Foundation
2370+#
2371+# lava-tool is distributed in the hope that it will be useful,
2372+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2373+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2374+# GNU General Public License for more details.
2375+#
2376+# You should have received a copy of the GNU Lesser General Public License
2377+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
2378+
2379+"""
2380+Tests for lava.commands.
2381+"""
2382+
2383+import os
2384+import tempfile
2385+
2386+from mock import (
2387+ MagicMock,
2388+ patch
2389+)
2390+
2391+from lava.commands import (
2392+ init,
2393+ submit,
2394+)
2395+from lava.config import Config
2396+from lava.helper.tests.helper_test import HelperTest
2397+from lava.tool.errors import CommandError
2398+
2399+
2400+class InitCommandTests(HelperTest):
2401+
2402+ def setUp(self):
2403+ super(InitCommandTests, self).setUp()
2404+ self.config_file = self.tmp("init_command_tests")
2405+ self.config = Config()
2406+ self.config.config_file = self.config_file
2407+
2408+ def tearDown(self):
2409+ super(InitCommandTests, self).tearDown()
2410+ if os.path.isfile(self.config_file):
2411+ os.unlink(self.config_file)
2412+
2413+ def test_register_arguments(self):
2414+ self.args.DIR = os.path.join(tempfile.gettempdir(), "a_fake_dir")
2415+ init_command = init(self.parser, self.args)
2416+ init_command.register_arguments(self.parser)
2417+
2418+ # Make sure we do not forget about this test.
2419+ self.assertEqual(2, len(self.parser.method_calls))
2420+
2421+ _, args, _ = self.parser.method_calls[0]
2422+ self.assertIn("--non-interactive", args)
2423+
2424+ _, args, _ = self.parser.method_calls[1]
2425+ self.assertIn("DIR", args)
2426+
2427+
2428+ @patch("lava.commands.edit_file", create=True)
2429+ def test_command_invoke_0(self, mocked_edit_file):
2430+ # Invoke the init command passing a path to a file. Should raise an
2431+ # exception.
2432+ self.args.DIR = self.temp_file.name
2433+ init_command = init(self.parser, self.args)
2434+ self.assertRaises(CommandError, init_command.invoke)
2435+
2436+ def test_command_invoke_2(self):
2437+ # Invoke the init command passing a path where the user cannot write.
2438+ try:
2439+ self.args.DIR = "/root/a_temp_dir"
2440+ init_command = init(self.parser, self.args)
2441+ self.assertRaises(CommandError, init_command.invoke)
2442+ finally:
2443+ if os.path.exists(self.args.DIR):
2444+ os.removedirs(self.args.DIR)
2445+
2446+ def test_update_data(self):
2447+ # Make sure the template is updated accordingly with the provided data.
2448+ self.args.DIR = self.temp_file.name
2449+
2450+ init_command = init(self.parser, self.args)
2451+ init_command.config.get = MagicMock()
2452+ init_command.config.save = MagicMock()
2453+ init_command.config.get.side_effect = ["a_job.json"]
2454+
2455+ expected = {
2456+ "jobfile": "a_job.json",
2457+ }
2458+
2459+ obtained = init_command._update_data()
2460+ self.assertEqual(expected, obtained)
2461+
2462+
2463+class SubmitCommandTests(HelperTest):
2464+ def setUp(self):
2465+ super(SubmitCommandTests, self).setUp()
2466+ self.config_file = self.tmp("submit_command_tests")
2467+ self.config = Config()
2468+ self.config.config_file = self.config_file
2469+ self.config.save = MagicMock()
2470+
2471+ def tearDown(self):
2472+ super(SubmitCommandTests, self).tearDown()
2473+ if os.path.isfile(self.config_file):
2474+ os.unlink(self.config_file)
2475+
2476+ def test_register_arguments(self):
2477+ self.args.JOB = os.path.join(tempfile.gettempdir(), "a_fake_file")
2478+ submit_command = submit(self.parser, self.args)
2479+ submit_command.register_arguments(self.parser)
2480+
2481+ # Make sure we do not forget about this test.
2482+ self.assertEqual(2, len(self.parser.method_calls))
2483+
2484+ _, args, _ = self.parser.method_calls[0]
2485+ self.assertIn("--non-interactive", args)
2486+
2487+ _, args, _ = self.parser.method_calls[1]
2488+ self.assertIn("JOB", args)
2489
2490=== modified file 'lava/tests/test_config.py'
2491--- lava/tests/test_config.py 2013-07-25 15:43:25 +0000
2492+++ lava/tests/test_config.py 2013-07-25 15:43:26 +0000
2493@@ -20,69 +20,46 @@
2494 lava.config unit tests.
2495 """
2496
2497-import os
2498 import sys
2499-import tempfile
2500
2501 from StringIO import StringIO
2502-from mock import MagicMock, patch, call
2503+from mock import (
2504+ MagicMock,
2505+ call,
2506+ patch,
2507+)
2508
2509 from lava.config import (
2510 Config,
2511 InteractiveConfig,
2512- ConfigParser,
2513 )
2514 from lava.helper.tests.helper_test import HelperTest
2515-from lava.parameter import Parameter
2516+from lava.parameter import (
2517+ Parameter,
2518+ ListParameter,
2519+)
2520 from lava.tool.errors import CommandError
2521
2522
2523-class MockedConfig(Config):
2524- """A subclass of the original Config class.
2525-
2526- Used to test the Config class, but to not have the same constructor in
2527- order to use temporary files for the configuration.
2528- """
2529- def __init__(self, config_file):
2530- self._cache = {}
2531- self._config_file = config_file
2532- self._config_backend = ConfigParser()
2533- self._config_backend.read([self._config_file])
2534-
2535-
2536-class MockedInteractiveConfig(InteractiveConfig):
2537- def __init__(self, config_file, force_interactive=False):
2538- self._cache = {}
2539- self._config_file = config_file
2540- self._config_backend = ConfigParser()
2541- self._config_backend.read([self._config_file])
2542- self._force_interactive = force_interactive
2543-
2544-
2545 class ConfigTestCase(HelperTest):
2546 """General test case class for the different Config classes."""
2547 def setUp(self):
2548 super(ConfigTestCase, self).setUp()
2549- self.config_file = tempfile.NamedTemporaryFile(delete=False)
2550-
2551 self.param1 = Parameter("foo")
2552 self.param2 = Parameter("bar", depends=self.param1)
2553
2554- def tearDown(self):
2555- super(ConfigTestCase, self).tearDown()
2556- if os.path.isfile(self.config_file.name):
2557- os.unlink(self.config_file.name)
2558-
2559
2560 class ConfigTest(ConfigTestCase):
2561
2562- def setUp(self):
2563+ @patch("lava.config.Config.save")
2564+ def setUp(self, mocked_save):
2565 super(ConfigTest, self).setUp()
2566- self.config = MockedConfig(self.config_file.name)
2567+ self.config = Config()
2568+ self.config.config_file = self.temp_file.name
2569
2570 def test_assert_temp_config_file(self):
2571 # Dummy test to make sure we are overriding correctly the Config class.
2572- self.assertEqual(self.config._config_file, self.config_file.name)
2573+ self.assertEqual(self.config.config_file, self.temp_file.name)
2574
2575 def test_config_put_in_cache_0(self):
2576 self.config._put_in_cache("key", "value", "section")
2577@@ -167,46 +144,38 @@
2578
2579 expected = "[DEFAULT]\nfoo = foo\n\n"
2580 obtained = ""
2581- with open(self.config_file.name) as tmp_file:
2582+ with open(self.temp_file.name) as tmp_file:
2583 obtained = tmp_file.read()
2584 self.assertEqual(expected, obtained)
2585
2586- @patch("lava.config.AT_EXIT_CALLS", spec=set)
2587- def test_config_atexit_call_list(self, mocked_calls):
2588- # Tests that the save() method is added to the set of atexit calls.
2589- config = Config()
2590- config._config_file = self.config_file.name
2591- config.put_parameter(self.param1, "foo")
2592-
2593- expected = [call.add(config.save)]
2594-
2595- self.assertEqual(expected, mocked_calls.mock_calls)
2596-
2597
2598 class InteractiveConfigTest(ConfigTestCase):
2599
2600- def setUp(self):
2601+ @patch("lava.config.Config.save")
2602+ def setUp(self, mocked_save):
2603 super(InteractiveConfigTest, self).setUp()
2604- self.config = MockedInteractiveConfig(
2605- config_file=self.config_file.name)
2606+ self.config = InteractiveConfig()
2607+ self.config.config_file = self.temp_file.name
2608
2609 @patch("lava.config.Config.get", new=MagicMock(return_value=None))
2610 def test_non_interactive_config_0(self):
2611- # Mocked config default is not to be interactive.
2612 # Try to get a value that does not exists, users just press enter when
2613 # asked for a value. Value will be empty.
2614+ self.config.force_interactive = False
2615 sys.stdin = StringIO("\n")
2616 value = self.config.get(Parameter("foo"))
2617 self.assertEqual("", value)
2618
2619 @patch("lava.config.Config.get", new=MagicMock(return_value="value"))
2620 def test_non_interactive_config_1(self):
2621- # Parent class config returns a value, but we are not interactive.
2622+ # Parent class config returns value, but we are not interactive.
2623+ self.config.force_interactive = False
2624 value = self.config.get(Parameter("foo"))
2625 self.assertEqual("value", value)
2626
2627 @patch("lava.config.Config.get", new=MagicMock(return_value=None))
2628 def test_non_interactive_config_2(self):
2629+ self.config.force_interactive = False
2630 expected = "bar"
2631 sys.stdin = StringIO(expected)
2632 value = self.config.get(Parameter("foo"))
2633@@ -216,7 +185,7 @@
2634 def test_interactive_config_0(self):
2635 # We force to be interactive, meaning that even if a value is found,
2636 # it will be asked anyway.
2637- self.config._force_interactive = True
2638+ self.config.force_interactive = True
2639 expected = "a_new_value"
2640 sys.stdin = StringIO(expected)
2641 value = self.config.get(Parameter("foo"))
2642@@ -226,28 +195,28 @@
2643 def test_interactive_config_1(self):
2644 # Force to be interactive, but when asked for the new value press
2645 # Enter. The old value should be returned.
2646- self.config._force_interactive = True
2647+ self.config.force_interactive = True
2648 sys.stdin = StringIO("\n")
2649 value = self.config.get(Parameter("foo"))
2650 self.assertEqual("value", value)
2651
2652 def test_calculate_config_section_0(self):
2653- self.config._force_interactive = True
2654+ self.config.force_interactive = True
2655 obtained = self.config._calculate_config_section(self.param1)
2656 expected = "DEFAULT"
2657 self.assertEqual(expected, obtained)
2658
2659 def test_calculate_config_section_1(self):
2660+ self.param1.set("foo")
2661 self.param2.depends.asked = True
2662- self.config._force_interactive = True
2663- self.config.put(self.param1.id, "foo")
2664+ self.config.force_interactive = True
2665 obtained = self.config._calculate_config_section(self.param2)
2666 expected = "foo=foo"
2667 self.assertEqual(expected, obtained)
2668
2669 def test_calculate_config_section_2(self):
2670- self.config._force_interactive = True
2671- self.config._config_backend.get = MagicMock(return_value=None)
2672+ self.config.force_interactive = True
2673+ self.config.config_backend.get = MagicMock(return_value=None)
2674 sys.stdin = StringIO("baz")
2675 expected = "foo=baz"
2676 obtained = self.config._calculate_config_section(self.param2)
2677@@ -256,10 +225,9 @@
2678 def test_calculate_config_section_3(self):
2679 # Tests that when a parameter has its value in the cache and also on
2680 # file, we honor the cached version.
2681+ self.param1.set("bar")
2682 self.param2.depends.asked = True
2683- self.config._force_interactive = True
2684- self.config._get_from_cache = MagicMock(return_value="bar")
2685- self.config._config_backend.get = MagicMock(return_value="baz")
2686+ self.config.force_interactive = True
2687 expected = "foo=bar"
2688 obtained = self.config._calculate_config_section(self.param2)
2689 self.assertEqual(expected, obtained)
2690@@ -273,6 +241,32 @@
2691
2692 mocked_raw.side_effect = KeyboardInterrupt()
2693
2694- self.config._force_interactive = True
2695+ self.config.force_interactive = True
2696 self.config.get(self.param1)
2697 self.assertTrue(mocked_sys_exit.called)
2698+
2699+ @patch("lava.parameter.raw_input", create=True)
2700+ def test_interactive_config_with_list_parameter(self, mocked_raw_input):
2701+ # Tests that we get a list back in the Config class when using
2702+ # ListParameter and that it contains the expected values.
2703+ expected = ["foo", "bar"]
2704+ mocked_raw_input.side_effect = expected + ["\n"]
2705+ obtained = self.config.get(ListParameter("list"))
2706+ self.assertIsInstance(obtained, list)
2707+ self.assertEqual(expected, obtained)
2708+
2709+ def test_interactive_save_list_param(self):
2710+ # Tests that when saved to file, the ListParameter parameter is stored
2711+ # correctly.
2712+ param_values = ["foo", "more than one words", "bar"]
2713+ list_param = ListParameter("list")
2714+ list_param.set(param_values)
2715+
2716+ self.config.put_parameter(list_param, param_values)
2717+ self.config.save()
2718+
2719+ expected = "[DEFAULT]\nlist = " + ",".join(param_values) + "\n\n"
2720+ obtained = ""
2721+ with open(self.temp_file.name, "r") as read_file:
2722+ obtained = read_file.read()
2723+ self.assertEqual(expected, obtained)
2724
2725=== modified file 'lava/tests/test_parameter.py'
2726--- lava/tests/test_parameter.py 2013-07-25 15:43:25 +0000
2727+++ lava/tests/test_parameter.py 2013-07-25 15:43:26 +0000
2728@@ -20,16 +20,32 @@
2729 lava.parameter unit tests.
2730 """
2731
2732-import sys
2733-
2734-from StringIO import StringIO
2735 from mock import patch
2736
2737 from lava.helper.tests.helper_test import HelperTest
2738-from lava.parameter import Parameter
2739-
2740-
2741-class ParameterTest(HelperTest):
2742+from lava.parameter import (
2743+ ListParameter,
2744+ Parameter,
2745+)
2746+
2747+from lava_tool.utils import to_list
2748+
2749+
2750+class GeneralParameterTest(HelperTest):
2751+ """General class with setUp and tearDown methods for Parameter tests."""
2752+ def setUp(self):
2753+ super(GeneralParameterTest, self).setUp()
2754+ # Patch class raw_input, start it, and stop it on tearDown.
2755+ self.patcher1 = patch("lava.parameter.raw_input", create=True)
2756+ self.mocked_raw_input = self.patcher1.start()
2757+
2758+ def tearDown(self):
2759+ super(GeneralParameterTest, self).tearDown()
2760+ self.patcher1.stop()
2761+
2762+
2763+class ParameterTest(GeneralParameterTest):
2764+ """Tests for the Parameter class."""
2765
2766 def setUp(self):
2767 super(ParameterTest, self).setUp()
2768@@ -38,14 +54,79 @@
2769 def test_prompt_0(self):
2770 # Tests that when we have a value in the parameters and the user press
2771 # Enter, we get the old value back.
2772- sys.stdin = StringIO("\n")
2773+ self.mocked_raw_input.return_value = "\n"
2774 obtained = self.parameter1.prompt()
2775 self.assertEqual(self.parameter1.value, obtained)
2776
2777- @patch("lava.parameter.raw_input", create=True)
2778- def test_prompt_1(self, mocked_raw_input):
2779+ def test_prompt_1(self,):
2780 # Tests that with a value stored in the parameter, if and EOFError is
2781 # raised when getting user input, we get back the old value.
2782- mocked_raw_input.side_effect = EOFError()
2783+ self.mocked_raw_input.side_effect = EOFError()
2784 obtained = self.parameter1.prompt()
2785 self.assertEqual(self.parameter1.value, obtained)
2786+
2787+ def test_to_list_0(self):
2788+ value = "a_value"
2789+ expected = [value]
2790+ obtained = to_list(value)
2791+ self.assertIsInstance(obtained, list)
2792+ self.assertEquals(expected, obtained)
2793+
2794+ def test_to_list_1(self):
2795+ expected = ["a_value", "b_value"]
2796+ obtained = to_list(expected)
2797+ self.assertIsInstance(obtained, list)
2798+ self.assertEquals(expected, obtained)
2799+
2800+class ListParameterTest(GeneralParameterTest):
2801+ """Tests for the specialized ListParameter class."""
2802+
2803+ def setUp(self):
2804+ super(ListParameterTest, self).setUp()
2805+ self.list_parameter = ListParameter("list")
2806+
2807+ def test_prompt_0(self):
2808+ # Test that when pressing Enter, the prompt stops and the list is
2809+ # returned.
2810+ expected = []
2811+ self.mocked_raw_input.return_value = "\n"
2812+ obtained = self.list_parameter.prompt()
2813+ self.assertEqual(expected, obtained)
2814+
2815+ def test_prompt_1(self):
2816+ # Tests that when passing 3 values, a list with those values
2817+ # is returned
2818+ expected = ["foo", "bar", "foobar"]
2819+ self.mocked_raw_input.side_effect = expected + ["\n"]
2820+ obtained = self.list_parameter.prompt()
2821+ self.assertEqual(expected, obtained)
2822+
2823+ def test_serialize_0(self):
2824+ # Tests the serialize method of ListParameter passing a list.
2825+ expected = "foo,bar,baz,1"
2826+ to_serialize = ["foo", "bar", "baz", "", 1]
2827+
2828+ obtained = self.list_parameter.serialize(to_serialize)
2829+ self.assertEqual(expected, obtained)
2830+
2831+ def test_serialize_1(self):
2832+ # Tests the serialize method of ListParameter passing an int.
2833+ expected = "1"
2834+ to_serialize = 1
2835+
2836+ obtained = self.list_parameter.serialize(to_serialize)
2837+ self.assertEqual(expected, obtained)
2838+
2839+ def test_deserialize_0(self):
2840+ # Tests the deserialize method of ListParameter with a string
2841+ # of values.
2842+ expected = ["foo", "bar", "baz"]
2843+ to_deserialize = "foo,bar,,baz,"
2844+ obtained = self.list_parameter.deserialize(to_deserialize)
2845+ self.assertEqual(expected, obtained)
2846+
2847+ def test_deserialize_1(self):
2848+ # Tests the deserialization method of ListParameter passing a list.
2849+ expected = ["foo", 1, "", "bar"]
2850+ obtained = self.list_parameter.deserialize(expected)
2851+ self.assertEqual(expected, obtained)
2852
2853=== modified file 'lava_tool/tests/__init__.py'
2854--- lava_tool/tests/__init__.py 2013-07-25 15:43:25 +0000
2855+++ lava_tool/tests/__init__.py 2013-07-25 15:43:26 +0000
2856@@ -35,19 +35,22 @@
2857
2858 def test_modules():
2859 return [
2860+ 'lava.device.tests.test_commands',
2861+ 'lava.device.tests.test_device',
2862+ 'lava.helper.tests.test_command',
2863+ 'lava.helper.tests.test_dispatcher',
2864+ 'lava.helper.tests.test_template',
2865+ 'lava.job.tests.test_commands',
2866+ 'lava.job.tests.test_job',
2867+ 'lava.testdef.tests.test_commands',
2868+ 'lava.tests.test_commands',
2869+ 'lava.tests.test_config',
2870+ 'lava.tests.test_parameter',
2871+ 'lava_dashboard_tool.tests.test_commands',
2872+ 'lava_tool.tests.test_auth_commands',
2873 'lava_tool.tests.test_authtoken',
2874- 'lava_tool.tests.test_auth_commands',
2875 'lava_tool.tests.test_commands',
2876 'lava_tool.tests.test_utils',
2877- 'lava_dashboard_tool.tests.test_commands',
2878- 'lava.job.tests.test_job',
2879- 'lava.job.tests.test_commands',
2880- 'lava.device.tests.test_device',
2881- 'lava.device.tests.test_commands',
2882- 'lava.tests.test_config',
2883- 'lava.tests.test_parameter',
2884- 'lava.helper.tests.test_command',
2885- 'lava.helper.tests.test_dispatcher',
2886 ]
2887
2888
2889
2890=== modified file 'lava_tool/tests/test_utils.py'
2891--- lava_tool/tests/test_utils.py 2013-07-25 15:43:25 +0000
2892+++ lava_tool/tests/test_utils.py 2013-07-25 15:43:26 +0000
2893@@ -18,16 +18,45 @@
2894
2895 """lava_tool.utils tests."""
2896
2897+import sys
2898+import os
2899 import subprocess
2900+import tempfile
2901
2902 from unittest import TestCase
2903-from mock import patch
2904+from mock import (
2905+ MagicMock,
2906+ call,
2907+ patch,
2908+)
2909
2910-from lava_tool.utils import has_command
2911+from lava.tool.errors import CommandError
2912+from lava_tool.utils import (
2913+ can_edit_file,
2914+ edit_file,
2915+ execute,
2916+ has_command,
2917+ retrieve_file,
2918+ verify_file_extension,
2919+)
2920
2921
2922 class UtilTests(TestCase):
2923
2924+ def setUp(self):
2925+ self.original_stdout = sys.stdout
2926+ sys.stdout = open("/dev/null", "w")
2927+ self.original_stderr = sys.stderr
2928+ sys.stderr = open("/dev/null", "w")
2929+ self.original_stdin = sys.stdin
2930+ self.temp_file = tempfile.NamedTemporaryFile(delete=False)
2931+
2932+ def tearDown(self):
2933+ sys.stdin = self.original_stdin
2934+ sys.stdout = self.original_stdout
2935+ sys.stderr = self.original_stderr
2936+ os.unlink(self.temp_file.name)
2937+
2938 @patch("lava_tool.utils.subprocess.check_call")
2939 def test_has_command_0(self, mocked_check_call):
2940 # Make sure we raise an exception when the subprocess is called.
2941@@ -39,3 +68,149 @@
2942 # Check that a "command" exists. The call to subprocess is mocked.
2943 mocked_check_call.return_value = 0
2944 self.assertTrue(has_command(""))
2945+
2946+ def test_verify_file_extension_with_extension(self):
2947+ extension = ".test"
2948+ supported = [extension[1:]]
2949+ try:
2950+ temp_file = tempfile.NamedTemporaryFile(suffix=extension,
2951+ delete=False)
2952+ obtained = verify_file_extension(
2953+ temp_file.name, extension[1:], supported)
2954+ self.assertEquals(temp_file.name, obtained)
2955+ finally:
2956+ if os.path.isfile(temp_file.name):
2957+ os.unlink(temp_file.name)
2958+
2959+ def test_verify_file_extension_without_extension(self):
2960+ extension = "json"
2961+ supported = [extension]
2962+ expected = "/tmp/a_fake.json"
2963+ obtained = verify_file_extension("/tmp/a_fake", extension, supported)
2964+ self.assertEquals(expected, obtained)
2965+
2966+ def test_verify_file_extension_with_unsupported_extension(self):
2967+ extension = "json"
2968+ supported = [extension]
2969+ expected = "/tmp/a_fake.json"
2970+ obtained = verify_file_extension(
2971+ "/tmp/a_fake.extension", extension, supported)
2972+ self.assertEquals(expected, obtained)
2973+
2974+ @patch("os.listdir")
2975+ def test_retrieve_job_file_0(self, mocked_os_listdir):
2976+ # Make sure that exception is raised if we go through all the elements
2977+ # returned by os.listdir().
2978+ mocked_os_listdir.return_value = ["a_file"]
2979+ self.assertRaises(CommandError, retrieve_file,
2980+ "a_path", ["ext"])
2981+
2982+ @patch("os.listdir")
2983+ def test_retrieve_job_file_1(self, mocked_os_listdir):
2984+ # Pass some files and directories to retrieve_file(), and make
2985+ # sure a file with .json suffix is returned.
2986+ # Pass also a hidden file.
2987+ try:
2988+ json_file = tempfile.NamedTemporaryFile(suffix=".json")
2989+ json_file_name = os.path.basename(json_file.name)
2990+
2991+ file_name_no_suffix = tempfile.NamedTemporaryFile(delete=False)
2992+ file_name_with_suffix = tempfile.NamedTemporaryFile(
2993+ suffix=".bork", delete=False)
2994+
2995+ temp_dir_name = "submit_command_test_tmp_dir"
2996+ temp_dir_path = os.path.join(tempfile.gettempdir(), temp_dir_name)
2997+ os.makedirs(temp_dir_path)
2998+
2999+ hidden_file = tempfile.NamedTemporaryFile(
3000+ prefix=".tmp", delete=False)
3001+
3002+ mocked_os_listdir.return_value = [
3003+ temp_dir_name, file_name_no_suffix.name,
3004+ file_name_with_suffix.name, json_file_name, hidden_file.name]
3005+
3006+ obtained = retrieve_file(tempfile.gettempdir(), ["json"])
3007+ self.assertEqual(json_file.name, obtained)
3008+ finally:
3009+ os.removedirs(temp_dir_path)
3010+ os.unlink(file_name_no_suffix.name)
3011+ os.unlink(file_name_with_suffix.name)
3012+ os.unlink(hidden_file.name)
3013+
3014+ @patch("lava_tool.utils.subprocess")
3015+ def test_execute_0(self, mocked_subprocess):
3016+ mocked_subprocess.check_call = MagicMock()
3017+ execute("foo")
3018+ self.assertEqual(mocked_subprocess.check_call.call_args_list,
3019+ [call(["foo"])])
3020+ self.assertTrue(mocked_subprocess.check_call.called)
3021+
3022+ @patch("lava_tool.utils.subprocess.check_call")
3023+ def test_execute_1(self, mocked_check_call):
3024+ mocked_check_call.side_effect = subprocess.CalledProcessError(1, "foo")
3025+ self.assertRaises(CommandError, execute, ["foo"])
3026+
3027+ @patch("lava_tool.utils.subprocess")
3028+ @patch("lava_tool.utils.has_command", return_value=False)
3029+ @patch("lava_tool.utils.os.environ.get", return_value=None)
3030+ @patch("lava_tool.utils.sys.exit")
3031+ def test_edit_file_0(self, mocked_sys_exit, mocked_env_get,
3032+ mocked_has_command, mocked_subprocess):
3033+ edit_file(self.temp_file.name)
3034+ self.assertTrue(mocked_sys_exit.called)
3035+
3036+ @patch("lava_tool.utils.subprocess")
3037+ @patch("lava_tool.utils.has_command", side_effect=[True, False])
3038+ @patch("lava_tool.utils.os.environ.get", return_value=None)
3039+ def test_edit_file_1(self, mocked_env_get, mocked_has_command,
3040+ mocked_subprocess):
3041+ mocked_subprocess.Popen = MagicMock()
3042+ edit_file(self.temp_file.name)
3043+ expected = [call(["sensible-editor", self.temp_file.name])]
3044+ self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
3045+
3046+ @patch("lava_tool.utils.subprocess")
3047+ @patch("lava_tool.utils.has_command", side_effect=[False, True])
3048+ @patch("lava_tool.utils.os.environ.get", return_value=None)
3049+ def test_edit_file_2(self, mocked_env_get, mocked_has_command,
3050+ mocked_subprocess):
3051+ mocked_subprocess.Popen = MagicMock()
3052+ edit_file(self.temp_file.name)
3053+ expected = [call(["xdg-open", self.temp_file.name])]
3054+ self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
3055+
3056+ @patch("lava_tool.utils.subprocess")
3057+ @patch("lava_tool.utils.has_command", return_value=False)
3058+ @patch("lava_tool.utils.os.environ.get", return_value="vim")
3059+ def test_edit_file_3(self, mocked_env_get, mocked_has_command,
3060+ mocked_subprocess):
3061+ mocked_subprocess.Popen = MagicMock()
3062+ edit_file(self.temp_file.name)
3063+ expected = [call(["vim", self.temp_file.name])]
3064+ self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
3065+
3066+ @patch("lava_tool.utils.subprocess")
3067+ @patch("lava_tool.utils.has_command", return_value=False)
3068+ @patch("lava_tool.utils.os.environ.get", return_value="vim")
3069+ def test_edit_file_4(self, mocked_env_get, mocked_has_command,
3070+ mocked_subprocess):
3071+ mocked_subprocess.Popen = MagicMock()
3072+ mocked_subprocess.Popen.side_effect = Exception()
3073+ self.assertRaises(CommandError, edit_file, self.temp_file.name)
3074+
3075+ def test_can_edit_file(self):
3076+ # Tests the can_edit_file method of the config command.
3077+ # This is to make sure the device config file is not erased when
3078+ # checking if it is possible to open it.
3079+ expected = ("hostname = a_fake_panda02\nconnection_command = \n"
3080+ "device_type = panda\n")
3081+
3082+ with open(self.temp_file.name, "w") as f:
3083+ f.write(expected)
3084+
3085+ self.assertTrue(can_edit_file(self.temp_file.name))
3086+ obtained = ""
3087+ with open(self.temp_file.name) as f:
3088+ obtained = f.read()
3089+
3090+ self.assertEqual(expected, obtained)
3091
3092=== modified file 'lava_tool/utils.py'
3093--- lava_tool/utils.py 2013-07-25 15:43:25 +0000
3094+++ lava_tool/utils.py 2013-07-25 15:43:26 +0000
3095@@ -16,8 +16,17 @@
3096 # You should have received a copy of the GNU Lesser General Public License
3097 # along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
3098
3099+import StringIO
3100+import base64
3101 import os
3102+import tarfile
3103+import tempfile
3104+import types
3105 import subprocess
3106+import sys
3107+import urlparse
3108+
3109+from lava.tool.errors import CommandError
3110
3111
3112 def has_command(command):
3113@@ -32,3 +41,280 @@
3114 except subprocess.CalledProcessError:
3115 command_available = False
3116 return command_available
3117+
3118+
3119+def to_list(value):
3120+ """Return a list from the passed value.
3121+
3122+ :param value: The parameter to turn into a list.
3123+ """
3124+ return_value = []
3125+ if isinstance(value, types.StringType):
3126+ return_value = [value]
3127+ else:
3128+ return_value = list(value)
3129+ return return_value
3130+
3131+
3132+def create_tar(paths):
3133+ """Creates a temporary tar file with the provided paths.
3134+
3135+ The tar file is not deleted at the end, it has to be delete by who calls
3136+ this function.
3137+
3138+ If just a directory is passed, it will be flattened out: its contents will
3139+ be added, but not the directory itself.
3140+
3141+ :param paths: List of paths to be included in the tar archive.
3142+ :type list
3143+ :return The path to the temporary tar file.
3144+ """
3145+ paths = to_list(paths)
3146+ try:
3147+ temp_tar_file = tempfile.NamedTemporaryFile(suffix=".tar",
3148+ delete=False)
3149+ with tarfile.open(temp_tar_file.name, "w") as tar_file:
3150+ for path in paths:
3151+ full_path = os.path.abspath(path)
3152+ if os.path.isfile(full_path):
3153+ arcname = os.path.basename(full_path)
3154+ tar_file.add(full_path, arcname=arcname)
3155+ elif os.path.isdir(full_path):
3156+ # If we pass a directory, flatten it out.
3157+ # List its contents, and add them as they are.
3158+ for element in os.listdir(full_path):
3159+ arcname = element
3160+ tar_file.add(os.path.join(full_path, element),
3161+ arcname=arcname)
3162+ return temp_tar_file.name
3163+ except tarfile.TarError:
3164+ raise CommandError("Error creating the temporary tar archive.")
3165+
3166+
3167+def base64_encode(path):
3168+ """Encode in base64 the provided file.
3169+
3170+ :param path: The path to a file.
3171+ :return The file content encoded in base64.
3172+ """
3173+ if os.path.isfile(path):
3174+ encoded_content = StringIO.StringIO()
3175+
3176+ try:
3177+ with open(path) as read_file:
3178+ base64.encode(read_file, encoded_content)
3179+
3180+ return encoded_content.getvalue().strip()
3181+ except IOError:
3182+ raise CommandError("Cannot read file "
3183+ "'{0}'.".format(path))
3184+ else:
3185+ raise CommandError("Provided path does not exists or is not a file: "
3186+ "{0}.".format(path))
3187+
3188+
3189+def retrieve_file(path, extensions):
3190+ """Searches for a file that has one of the supported extensions.
3191+
3192+ The path of the first file that matches one of the supported provided
3193+ extensions will be returned. The files are examined in alphabetical
3194+ order.
3195+
3196+ :param path: Where to look for the file.
3197+ :param extensions: A list of extensions the file to look for should
3198+ have.
3199+ :return The full path of the file.
3200+ """
3201+ if os.path.isfile(path):
3202+ if check_valid_extension(path, extensions):
3203+ retrieved_path = path
3204+ else:
3205+ raise CommandError("The provided file '{0}' is not "
3206+ "valid: extension not supported.".format(path))
3207+ else:
3208+ dir_listing = os.listdir(path)
3209+ dir_listing.sort()
3210+
3211+ for element in dir_listing:
3212+ if element.startswith("."):
3213+ continue
3214+
3215+ element_path = os.path.join(path, element)
3216+ if os.path.isdir(element_path):
3217+ continue
3218+ elif os.path.isfile(element_path):
3219+ if check_valid_extension(element_path, extensions):
3220+ retrieved_path = element_path
3221+ break
3222+ else:
3223+ raise CommandError("No suitable file found in '{0}'".format(path))
3224+
3225+ return retrieved_path
3226+
3227+
3228+def check_valid_extension(path, extensions):
3229+ """Checks that a file has one of the supported extensions.
3230+
3231+ :param path: The file to check.
3232+ :param extensions: A list of supported extensions.
3233+ """
3234+ is_valid = False
3235+
3236+ local_path, file_name = os.path.split(path)
3237+ name, full_extension = os.path.splitext(file_name)
3238+
3239+ if full_extension:
3240+ extension = full_extension[1:].strip().lower()
3241+ if extension in extensions:
3242+ is_valid = True
3243+ return is_valid
3244+
3245+
3246+def verify_file_extension(path, default, supported):
3247+ """Verifies if a file has a supported extensions.
3248+
3249+ If the file does not have one, it will add the default extension
3250+ provided.
3251+
3252+ :param path: The path of a file to verify.
3253+ :param default: The default extension to use.
3254+ :param supported: A list of supported extensions to check against.
3255+ :return The path of the file.
3256+ """
3257+ full_path, file_name = os.path.split(path)
3258+ name, extension = os.path.splitext(file_name)
3259+ if not extension:
3260+ path = ".".join([path, default])
3261+ elif extension[1:].lower() not in supported:
3262+ path = os.path.join(full_path, ".".join([name, default]))
3263+ return path
3264+
3265+
3266+def verify_path_existance(path):
3267+ """Verifies if a given path exists or not on the file system.
3268+
3269+ Raises a CommandError in case it exists.
3270+
3271+ :param path: The path to verify."""
3272+ if os.path.exists(path):
3273+ raise CommandError("{0} already exists.".format(path))
3274+
3275+
3276+def write_file(path, content):
3277+ """Creates a file with the specified content.
3278+
3279+ :param path: The path of the file to write.
3280+ :param content: What to write in the file.
3281+ """
3282+ try:
3283+ with open(path, "w") as to_write:
3284+ to_write.write(content)
3285+ except (OSError, IOError):
3286+ raise CommandError("Error writing file '{0}'".format(path))
3287+
3288+
3289+def execute(cmd_args):
3290+ """Executes the supplied command args.
3291+
3292+ :param cmd_args: The command, and its optional arguments, to run.
3293+ :return The command execution return code.
3294+ """
3295+ cmd_args = to_list(cmd_args)
3296+ try:
3297+ return subprocess.check_call(cmd_args)
3298+ except subprocess.CalledProcessError:
3299+ raise CommandError("Error running the following command: "
3300+ "{0}".format(" ".join(cmd_args)))
3301+
3302+
3303+def can_edit_file(path):
3304+ """Checks if a file can be opend in write mode.
3305+
3306+ :param path: The path to the file.
3307+ :return True if it is possible to write on the file, False otherwise.
3308+ """
3309+ can_edit = True
3310+ try:
3311+ fp = open(path, "a")
3312+ fp.close()
3313+ except IOError:
3314+ can_edit = False
3315+ return can_edit
3316+
3317+
3318+def edit_file(file_to_edit):
3319+ """Opens the specified file with the default file editor.
3320+
3321+ :param file_to_edit: The file to edit.
3322+ """
3323+ editor = os.environ.get("EDITOR", None)
3324+ if editor is None:
3325+ if has_command("sensible-editor"):
3326+ editor = "sensible-editor"
3327+ elif has_command("xdg-open"):
3328+ editor = "xdg-open"
3329+ else:
3330+ # We really do not know how to open a file.
3331+ print >> sys.stdout, ("Cannot find an editor to open the "
3332+ "file '{0}'.".format(file_to_edit))
3333+ print >> sys.stdout, ("Either set the 'EDITOR' environment "
3334+ "variable, or install 'sensible-editor' "
3335+ "or 'xdg-open'.")
3336+ sys.exit(-1)
3337+ try:
3338+ subprocess.Popen([editor, file_to_edit]).wait()
3339+ except Exception:
3340+ raise CommandError("Error opening the file '{0}' with the "
3341+ "following editor: {1}.".format(file_to_edit,
3342+ editor))
3343+
3344+
3345+def verify_and_create_url(server, endpoint=""):
3346+ """Checks that the provided values make a correct URL.
3347+
3348+ If the server address does not contain a scheme, by default it will use
3349+ HTTPS.
3350+ The endpoint is then added at the URL.
3351+
3352+ :param server: A server URL to verify.
3353+ :return A URL.
3354+ """
3355+ scheme, netloc, path, params, query, fragment = \
3356+ urlparse.urlparse(server)
3357+ if not scheme:
3358+ scheme = "https"
3359+ if not netloc:
3360+ netloc, path = path, ""
3361+
3362+ if not netloc[-1:] == "/":
3363+ netloc += "/"
3364+
3365+ if endpoint:
3366+ if endpoint[0] == "/":
3367+ endpoint = endpoint[1:]
3368+ if not endpoint[-1:] == "/":
3369+ endpoint += "/"
3370+ netloc += endpoint
3371+
3372+ return urlparse.urlunparse(
3373+ (scheme, netloc, path, params, query, fragment))
3374+
3375+
3376+def create_dir(path, dir_name=None):
3377+ """Checks if a directory does not exists, and creates it.
3378+
3379+ :param path: The path where the directory should be created.
3380+ :param dir_name: An optional name for a directory to be created at
3381+ path (dir_name will be joined with path).
3382+ :return The path of the created directory."""
3383+ created_dir = path
3384+ if dir_name:
3385+ created_dir = os.path.join(path, dir_name)
3386+
3387+ if not os.path.isdir(created_dir):
3388+ try:
3389+ os.makedirs(created_dir)
3390+ except OSError:
3391+ raise CommandError("Cannot create directory "
3392+ "'{0}'.".format(created_dir))
3393+ return created_dir
3394
3395=== modified file 'setup.py'
3396--- setup.py 2013-07-25 15:43:25 +0000
3397+++ setup.py 2013-07-25 15:43:26 +0000
3398@@ -46,6 +46,7 @@
3399 "Topic :: Software Development :: Testing",
3400 ],
3401 install_requires=[
3402+ 'PyYAML >= 3.10',
3403 'argparse >= 1.1',
3404 'argcomplete >= 0.3',
3405 'keyring',

Subscribers

People subscribed via source and target branches