Merge lp:~milo/lava-tool/lava-165 into lp:~linaro-validation/lava-tool/trunk
- lava-165
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Antonio Terceiro | Pending | ||
Review via email:
|
Commit message
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.
- 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.

Antonio Terceiro (terceiro) wrote : | # |

Milo Casagrande (milo) wrote : | # |
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.
>> +
>> + if not os.path.
>> + # 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}'.
>> +
>> + with open(default_
>> + write_file.
>
> 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_
>> +
>> + # Make sure the script is executable.
>> + os.chmod(
>> + stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
>> +
>> + print >> sys.stdout, ("\nCreating test definition "
>> + "'{0}':
>
> 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_
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...

Antonio Terceiro (terceiro) wrote : | # |
> > 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(
> >> + data = raw_input(
> >> 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/
>
> 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_
Above there was a call to serialize, and maybe this should be handled
transparently inside this class. The factoring in serialize/
is good already.
> >> + :param value: The value to serialize.
> >> + :return The serialized value as string.
> >> + """
> >> + serialized = ""
> >> + if isinstance(value, list):
> >> + serialized = LIST_SERIALIZE_
> >> + 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 ...

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
- 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
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', |
On Tue, Jul 16, 2013 at 07:41:29AM -0000, Milo Casagrande wrote: /code.launchpad .net/~milo/ lava-tool/ lava-165/ +merge/ 174942
> 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:/
>
> 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' www.gnu. org/licenses/>.
> --- 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://
> +
> +"""
> +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...