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

Proposed by Milo Casagrande
Status: Merged
Merged at revision: 188
Proposed branch: lp:~milo/lava-tool/device-parameters
Merge into: lp:~linaro-validation/lava-tool/trunk
Prerequisite: lp:~milo/lava-tool/lava-168
Diff against target: 1648 lines (+1001/-234)
23 files modified
.bzrignore (+1/-0)
.coveragerc (+13/-0)
HACKING (+17/-0)
ci-build (+12/-0)
lava/config.py (+176/-75)
lava/device/__init__.py (+25/-56)
lava/device/commands.py (+3/-3)
lava/device/templates.py (+37/-9)
lava/device/tests/test_commands.py (+79/-4)
lava/device/tests/test_device.py (+52/-46)
lava/helper/command.py (+1/-1)
lava/helper/template.py (+44/-0)
lava/helper/tests/test_command.py (+70/-0)
lava/helper/tests/test_dispatcher.py (+27/-2)
lava/job/__init__.py (+2/-16)
lava/job/commands.py (+3/-2)
lava/job/templates.py (+1/-1)
lava/job/tests/test_commands.py (+6/-6)
lava/job/tests/test_job.py (+26/-13)
lava/parameter.py (+75/-0)
lava/tests/test_config.py (+278/-0)
lava/tests/test_parameter.py (+51/-0)
lava_tool/tests/__init__.py (+2/-0)
To merge this branch: bzr merge lp:~milo/lava-tool/device-parameters
Reviewer Review Type Date Requested Status
Antonio Terceiro Needs Fixing
Linaro Validation Team Pending
Review via email: mp+170653@code.launchpad.net

Description of the change

This new branch addresses some of the concerns raised while reviewing lp:~milo/lava-tool/lava-168.
It is in a separate branch since the previous merge proposal was already quite big.

* Refactored the Config class: now there are a generic Config class with most of the logic to store values, and an InteractiveConfig class that overrides only a subset of methods from Config in order to handle user input.
* The Parameter class has been refactored and moved into its own file. Two new attributes have been added: 'value', mostly needed for the Device templates; 'asked', needed when using the InteractiveConfig class in order not to ask twice the same parameter value.
* Tests have been added and old ones refactored.

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

Refactored constructor.

227. By Milo Casagrande

Fixed the device templates.

    * Needed to specialized some of the template parameters due to
      object references.

228. By Milo Casagrande

Fixed the Config classes.

    * Parameter values can also be empty.
    * If the user presses Enter with an old value, keep the old value,
      otherwise value will be empty.

229. By Milo Casagrande

Fixed tests.

230. By Milo Casagrande

Added two more tests.

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

Hi Milo,

Thanks for your efforts with this. I am amazed by the amount of tests you have
been adding. :-)

I have some comments below

 review needs-fixing

> === added file 'HACKING'
> --- HACKING 1970-01-01 00:00:00 +0000
> +++ HACKING 2013-06-20 15:51:34 +0000
> @@ -0,0 +1,17 @@
> +Tests Code Coverage
> +===================
> +
> +To have a nicely HTML viewable report on tests code coverage, do as follows:
> +
> +* Install `python-coverage` (`pip install coverage` in case you use pip)
> +* Run the following command:
> +
> + python-coverage run -m unittest lava_tool.tests.test_suite 2>/dev/null && python-coverage html
> +
> +* The report will be save in a directory called `lava_tool_coverage`: open

typo: save → saved

> +the `index.html` file in there to see the report.
> +
> +Notes:
> +
> + * To re-run the coverage report, you have to delete the `lava_tool_coverage`
> +directory first, otherwise `python-coverage` will fail.
>
> === modified file 'lava/config.py'
> --- lava/config.py 2013-06-20 15:51:34 +0000
> +++ lava/config.py 2013-06-20 15:51:34 +0000
> @@ -16,91 +16,224 @@
> # You should have received a copy of the GNU Lesser General Public License
> # along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
>
> +"""
> +Config class.
> +"""
> +
> import atexit
> -from ConfigParser import ConfigParser, NoOptionError, NoSectionError
> import os
> import readline
> -
> -__all__ = ['InteractiveConfig', 'NonInteractiveConfig']
> -
> -history = os.path.join(os.path.expanduser("~"), ".lava_history")
> +import sys
> +
> +
> +from ConfigParser import ConfigParser, NoOptionError, NoSectionError
> +
> +__all__ = ['Config', 'InteractiveConfig']
> +
> +# Store for function calls to be made at exit time.
> +AT_EXIT_CALLS = set()
> +# Config default section.
> +DEFAULT_SECTION = "DEFAULT"
> +
> +HISTORY = os.path.join(os.path.expanduser("~"), ".lava_history")
> try:
> - readline.read_history_file(history)
> + readline.read_history_file(HISTORY)
> except IOError:
> pass
> -atexit.register(readline.write_history_file, history)
> -
> -config_file = (os.environ.get('LAVACONFIG') or
> - os.path.join(os.path.expanduser('~'), '.lavaconfig'))
> -config_backend = ConfigParser()
> -config_backend.read([config_file])
> -
> -
> -def save_config():
> - with open(config_file, 'w') as f:
> - config_backend.write(f)
> -atexit.register(save_config)
> -
> -
> -class Parameter(object):
> -
> - def __init__(self, id, depends=None):
> - self.id = id
> - self.depends = depends
> -
> -
> -class InteractiveConfig(object):
> -
> - def __init__(self, force_interactive=False):
> - self._force_interactive = force_interactive
> +atexit.register(readline.write_history_file, HISTORY)
> +
> +
> +def _run_at_exit():
> + """Runs all the function at exit."""
> + for call in list(AT_EXIT_CALLS):
> + call()
> +atexit.register(_run_at_exit)
> +
> +
> +class Config(object):
> + """A generic config object."""
> + def __init__(self):
> + # The cache where to store parameters.
> self._cache = {}
> -
> - def get(self, parameter):
> - key = parameter.id
> - ...

review: Needs Fixing
lp:~milo/lava-tool/device-parameters updated
231. By Milo Casagrande

Fixed typos.

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

Hello Antonio!

Thanks for going through this!

On Fri, Jun 21, 2013 at 1:40 PM, Antonio Terceiro
<email address hidden> wrote:
>
>> +class InteractiveConfig(Config):
>> + """An interactive config.
>> +
>> + If a value is not found in the config file, it will ask it and then stores
>> + it.
>> + """
>> + def __init__(self, force_interactive=True):
>> + super(InteractiveConfig, self).__init__()
>> + self._force_interactive = force_interactive
>> +
>> + def _calculate_config_section(self, parameter):
>> + """Calculates the config section of the specified parameter.
>> +
>> + :param parameter: The parameter to calculate the section of.
>> + :type Parameter
>> + :return The config section.
>> + """
>> + section = DEFAULT_SECTION
>> + if parameter.depends:
>> + # This is mostly relevant to the InteractiveConfig class.
>> + # If a parameter has a dependency we do as follows:
>> + # - Get the dependency cached value
>> + # - Get the dependency value from the config file
>> + # - If both are None, it means the dependency has not been inserted
>> + # yet, and we ask for it.
>
> I have the impression that just calling get() would already to this.
>
>> + depend_section = self._calculate_config_section(parameter.depends)
>> +
>> + cached_value = self._get_from_cache(parameter.depends,
>> + depend_section)
>> + config_value = self._get_from_backend(parameter.depends,
>> + depend_section)
>> +
>> + # Honor the cached value.
>> + value = cached_value or config_value
>> + if not value:
>> + value = self.get(parameter.depends)
>> + parameter.depends.asked = True
>> + section = "{0}={1}".format(parameter.depends.id, value)
>> + return section
>
> Also, I don't understand why you need to override this method. I can't see why
> the logic for determining the section to write a config entry to would be
> different on interactive config wrt to plain config - as long as get() does the
> right thing.

Yeah, this is something I forgot to remove.
I had a problem in the device template and how I was "copying" the
default template, and when trying to figure out what was happening, I
override that method.
The two gets (from cache and config) were done to avoid multiple calls
to the _calculate_config_section method (that is why I added the
section parameter to get()).

>> -
>> - def _update(self):
>> - """Updates the template with the values specified for this class.
>> -
>> - Subclasses need to override this when they add more specific
>> - attributes.
>> + write_file.write(str(self))
>> +
>> + def update(self, config):
>> + """Updates the Device object values based on the provided config.
>> +
>> + :param config: A Config instance.
>> """
>> - # This is needed for the 'default' behavior. If we matched a known
>> - # device, we do not need...

Read more...

lp:~milo/lava-tool/device-parameters updated
232. By Milo Casagrande

Use copy instead of deepcopy.

    * Used copy instead of deepcopy to avoid wrong references
      when updating the template.

233. By Milo Casagrande

Removed overridden method.

    * The overridden method is not necessary.

234. By Milo Casagrande

Fixed typo and refactore function name.

235. By Milo Casagrande

Removed not necessary test.

236. By Milo Casagrande

Refactored where user input is taken.

    * Moved raw_input logic from the Config into the Parameter.
    * Fixed old tests.
    * Added new test class for the Parameter class.

237. By Milo Casagrande

Fixed how the prompt is built.

238. By Milo Casagrande

Merged missing commit.

239. By Milo Casagrande

Readded asked attribute.

240. By Milo Casagrande

Refactored the expand_template function.

    * Created a helper module for template.
    * Moved the expand_template function in the new module.
    * Updated lava job command to use the new helper function.

241. By Milo Casagrande

Removed unused import statement.

242. By Milo Casagrande

Added two more tests for the dispatcher helper functions.

243. By Milo Casagrande

Fixed list creation.

244. By Milo Casagrande

Added more tests for the helper command functions.

245. By Milo Casagrande

Fixed file test.

246. By Milo Casagrande

Added more tests to the device commands.

247. By Milo Casagrande

Added one more test for device commands.

248. By Milo Casagrande

Fixed import.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2013-06-27 12:48:26 +0000
3+++ .bzrignore 2013-06-27 12:48:26 +0000
4@@ -5,3 +5,4 @@
5 /tags
6 .testrepository
7 *.egg
8+lava_tool_coverage
9
10=== added file '.coveragerc'
11--- .coveragerc 1970-01-01 00:00:00 +0000
12+++ .coveragerc 2013-06-27 12:48:26 +0000
13@@ -0,0 +1,13 @@
14+[run]
15+branch = True
16+source = .
17+omit =
18+ setup*
19+
20+[report]
21+precision = 2
22+show_missing = True
23+
24+[html]
25+title = Code Coverage of lava-tool
26+directory = lava_tool_coverage
27
28=== added file 'HACKING'
29--- HACKING 1970-01-01 00:00:00 +0000
30+++ HACKING 2013-06-27 12:48:26 +0000
31@@ -0,0 +1,17 @@
32+Tests Code Coverage
33+===================
34+
35+To have a nicely HTML viewable report on tests code coverage, do as follows:
36+
37+* Install `python-coverage` (`pip install coverage` in case you use pip)
38+* Run the following command:
39+
40+ python-coverage run -m unittest lava_tool.tests.test_suite 2>/dev/null && python-coverage html
41+
42+* The report will be saved in a directory called `lava_tool_coverage`: open
43+the `index.html` file in there to see the report.
44+
45+Notes:
46+
47+ * To re-run the coverage report, you have to delete the `lava_tool_coverage`
48+directory first, otherwise `python-coverage` will fail.
49
50=== modified file 'ci-build'
51--- ci-build 2013-06-27 12:48:26 +0000
52+++ ci-build 2013-06-27 12:48:26 +0000
53@@ -1,6 +1,8 @@
54 #!/bin/sh
55
56 VENV_DIR="/tmp/ci-build-venv"
57+# Directory where coverage HTML report will be written.
58+COVERAGE_REPORT_DIR="lava_tool_coverage"
59
60 set -e
61
62@@ -26,6 +28,10 @@
63 if ! pip show mock | grep -q mock; then
64 pip install mock
65 fi
66+# Requirement to run code coverage tests.
67+if ! pip show coverage | grep -q coverage; then
68+ pip install coverage
69+fi
70
71 export LAVACONFIG=/dev/null
72
73@@ -45,4 +51,10 @@
74 python -m unittest lava_tool.tests.test_suite < /dev/null
75 fi
76
77+if test -d $COVERAGE_REPORT_DIR; then
78+ rm -rf $COVERAGE_REPORT_DIR
79+fi
80+# Runs python-coverage.
81+python-coverage run -m unittest lava_tool.tests.test_suite 2>/dev/null && python-coverage html
82+
83 ./integration-tests
84
85=== modified file 'lava/config.py'
86--- lava/config.py 2013-06-27 12:48:26 +0000
87+++ lava/config.py 2013-06-27 12:48:26 +0000
88@@ -16,91 +16,192 @@
89 # You should have received a copy of the GNU Lesser General Public License
90 # along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
91
92+"""
93+Config class.
94+"""
95+
96 import atexit
97-from ConfigParser import ConfigParser, NoOptionError, NoSectionError
98 import os
99 import readline
100
101-__all__ = ['InteractiveConfig', 'NonInteractiveConfig']
102-
103-history = os.path.join(os.path.expanduser("~"), ".lava_history")
104+from ConfigParser import (
105+ ConfigParser,
106+ NoOptionError,
107+ NoSectionError,
108+)
109+
110+from lava.tool.errors import CommandError
111+
112+__all__ = ['Config', 'InteractiveConfig']
113+
114+# Store for function calls to be made at exit time.
115+AT_EXIT_CALLS = set()
116+# Config default section.
117+DEFAULT_SECTION = "DEFAULT"
118+
119+HISTORY = os.path.join(os.path.expanduser("~"), ".lava_history")
120 try:
121- readline.read_history_file(history)
122+ readline.read_history_file(HISTORY)
123 except IOError:
124 pass
125-atexit.register(readline.write_history_file, history)
126-
127-config_file = (os.environ.get('LAVACONFIG') or
128- os.path.join(os.path.expanduser('~'), '.lavaconfig'))
129-config_backend = ConfigParser()
130-config_backend.read([config_file])
131-
132-
133-def save_config():
134- with open(config_file, 'w') as f:
135- config_backend.write(f)
136-atexit.register(save_config)
137-
138-
139-class Parameter(object):
140-
141- def __init__(self, id, depends=None):
142- self.id = id
143- self.depends = depends
144-
145-
146-class InteractiveConfig(object):
147-
148- def __init__(self, force_interactive=False):
149- self._force_interactive = force_interactive
150+atexit.register(readline.write_history_file, HISTORY)
151+
152+
153+def _run_at_exit():
154+ """Runs all the function at exit."""
155+ for call in list(AT_EXIT_CALLS):
156+ call()
157+atexit.register(_run_at_exit)
158+
159+
160+class Config(object):
161+ """A generic config object."""
162+ def __init__(self):
163+ # The cache where to store parameters.
164 self._cache = {}
165-
166- def get(self, parameter):
167- key = parameter.id
168- value = None
169+ self._config_file = (os.environ.get('LAVACONFIG') or
170+ os.path.join(os.path.expanduser('~'),
171+ '.lavaconfig'))
172+ self._config_backend = ConfigParser()
173+ self._config_backend.read([self._config_file])
174+ AT_EXIT_CALLS.add(self.save)
175+
176+ def _calculate_config_section(self, parameter):
177+ """Calculates the config section of the specified parameter.
178+
179+ :param parameter: The parameter to calculate the section of.
180+ :type Parameter
181+ :return The config section.
182+ """
183+ section = DEFAULT_SECTION
184 if parameter.depends:
185- pass
186- config_section = parameter.depends.id + '=' + self.get(parameter.depends)
187+ section = "{0}={1}".format(parameter.depends.id,
188+ self.get(parameter.depends))
189+ return section
190+
191+ def get(self, parameter, section=None):
192+ """Retrieves a Parameter value.
193+
194+ The value is taken either from the Parameter itself, or from the cache,
195+ or from the config file.
196+
197+ :param parameter: The parameter to search.
198+ :type Parameter
199+ :return The parameter value, or None if it is not found.
200+ """
201+ if not section:
202+ section = self._calculate_config_section(parameter)
203+ # Try to get the parameter value first if it has one.
204+ if parameter.value:
205+ value = parameter.value
206 else:
207- config_section = "DEFAULT"
208-
209- if config_section in self._cache:
210- if key in self._cache[config_section]:
211- return self._cache[config_section][key]
212-
213- prompt = '%s: ' % key
214-
215+ value = self._get_from_cache(parameter, section)
216+
217+ if value is None:
218+ value = self._get_from_backend(parameter, section)
219+ return value
220+
221+ def _get_from_backend(self, parameter, section):
222+ """Gets the parameter value from the config backend.
223+
224+ :param parameter: The Parameter to look up.
225+ :param section: The section in the Config.
226+ """
227+ value = None
228 try:
229- value = config_backend.get(config_section, key)
230+ value = self._config_backend.get(section, parameter.id)
231 except (NoOptionError, NoSectionError):
232+ # Ignore, we return None.
233 pass
234- if value:
235- if self._force_interactive:
236- prompt = "%s[%s]: " % (key, value)
237- else:
238- return value
239- try:
240- user_input = raw_input(prompt).strip()
241- except EOFError:
242- user_input = None
243- if user_input:
244- value = user_input
245- if not config_backend.has_section(config_section) and config_section != 'DEFAULT':
246- config_backend.add_section(config_section)
247- config_backend.set(config_section, key, value)
248-
249- if value:
250- if config_section not in self._cache:
251- self._cache[config_section] = {}
252- self._cache[config_section][key] = value
253- return value
254- else:
255- raise KeyError(key)
256-
257-class NonInteractiveConfig(object):
258-
259- def __init__(self, data):
260- self.data = data
261-
262- def get(self, parameter):
263- return self.data[parameter.id]
264+ return value
265+
266+ def _get_from_cache(self, parameter, section):
267+ """Looks for the specified parameter in the internal cache.
268+
269+ :param parameter: The parameter to search.
270+ :type Parameter
271+ :return The parameter value, of None if it is not found.
272+ """
273+ value = None
274+ if section in self._cache.keys():
275+ if parameter.id in self._cache[section].keys():
276+ value = self._cache[section][parameter.id]
277+ return value
278+
279+ def _put_in_cache(self, key, value, section=DEFAULT_SECTION):
280+ """Insert the passed parameter in the internal cache.
281+
282+ :param parameter: The parameter to insert.
283+ :type Parameter
284+ :param section: The name of the section in the config file.
285+ :type str
286+ """
287+ if section not in self._cache.keys():
288+ self._cache[section] = {}
289+ self._cache[section][key] = value
290+
291+ def put(self, key, value, section=DEFAULT_SECTION):
292+ """Adds a parameter to the config file.
293+
294+ :param key: The key to add.
295+ :param value: The value to add.
296+ :param section: The name of the section as in the config file.
297+ """
298+ if (not self._config_backend.has_section(section) and
299+ section != DEFAULT_SECTION):
300+ self._config_backend.add_section(section)
301+ self._config_backend.set(section, key, value)
302+ # Store in the cache too.
303+ self._put_in_cache(key, value, section)
304+
305+ def put_parameter(self, parameter, value=None, section=None):
306+ """Adds a Parameter to the config file and cache.
307+
308+ :param Parameter: The parameter to add.
309+ :param value: The value of the parameter. Defaults to None.
310+ :param section: The section where this parameter should be stored.
311+ Defaults to None.
312+ """
313+ if not section:
314+ section = self._calculate_config_section(parameter)
315+
316+ if value is None and parameter.value is not None:
317+ value = parameter.value
318+ elif value is None:
319+ raise CommandError("No value assigned to '{0}'.".format(
320+ parameter.id))
321+ self.put(parameter.id, value, section)
322+
323+ def save(self):
324+ """Saves the config to file."""
325+ with open(self._config_file, "w") as write_file:
326+ self._config_backend.write(write_file)
327+
328+
329+class InteractiveConfig(Config):
330+ """An interactive config.
331+
332+ If a value is not found in the config file, it will ask it and then stores
333+ it.
334+ """
335+ def __init__(self, force_interactive=True):
336+ super(InteractiveConfig, self).__init__()
337+ self._force_interactive = force_interactive
338+
339+ def get(self, parameter, section=None):
340+ """Overrides the parent one.
341+
342+ The only difference with the parent one, is that it will ask to type
343+ a parameter value in case it is not found.
344+ """
345+ if not section:
346+ section = self._calculate_config_section(parameter)
347+ value = super(InteractiveConfig, self).get(parameter, section)
348+
349+ if not (value is not None and parameter.asked):
350+ if not value or self._force_interactive:
351+ value = parameter.prompt(old_value=value)
352+
353+ if value is not None:
354+ self.put(parameter.id, value, section)
355+ return value
356
357=== modified file 'lava/device/__init__.py'
358--- lava/device/__init__.py 2013-06-27 12:48:26 +0000
359+++ lava/device/__init__.py 2013-06-27 12:48:26 +0000
360@@ -18,11 +18,14 @@
361
362 import re
363
364+from copy import deepcopy
365+
366 from lava.device.templates import (
367+ DEFAULT_TEMPLATE,
368+ HOSTNAME_PARAMETER,
369 KNOWN_TEMPLATES,
370- DEFAULT_TEMPLATE,
371 )
372-from lava.tool.errors import CommandError
373+from lava.helper.template import expand_template
374
375
376 def __re_compile(name):
377@@ -44,60 +47,37 @@
378
379 class Device(object):
380 """A generic device."""
381- def __init__(self, hostname, template):
382- self.device_type = None
383+ def __init__(self, template, hostname=None):
384+ self.data = deepcopy(template)
385 self.hostname = hostname
386- self.template = template.copy()
387
388 def write(self, conf_file):
389 """Writes the object to file.
390
391 :param conf_file: The full path of the file where to write."""
392 with open(conf_file, 'w') as write_file:
393- write_file.write(self.__str__())
394-
395- def _update(self):
396- """Updates the template with the values specified for this class.
397-
398- Subclasses need to override this when they add more specific
399- attributes.
400+ write_file.write(str(self))
401+
402+ def update(self, config):
403+ """Updates the Device object values based on the provided config.
404+
405+ :param config: A Config instance.
406 """
407- # This is needed for the 'default' behavior. If we matched a known
408- # device, we do not need to update its device_type, since its already
409- # defined in the template.
410- if self.device_type:
411- self.template.update(hostname=self.hostname,
412- device_type=self.device_type)
413- else:
414- self.template.update(hostname=self.hostname)
415+ # We should always have a hostname, since it defaults to the name
416+ # given on the command line for the config file.
417+ if self.hostname is not None:
418+ # We do not ask the user again this parameter.
419+ self.data[HOSTNAME_PARAMETER.id].asked = True
420+ config.put(HOSTNAME_PARAMETER.id, self.hostname)
421+
422+ expand_template(self.data, config)
423
424 def __str__(self):
425- self._update()
426 string_list = []
427- for key, value in self.template.iteritems():
428- if not value:
429- value = ''
430+ for key, value in self.data.iteritems():
431 string_list.append("{0} = {1}\n".format(str(key), str(value)))
432 return "".join(string_list)
433
434- def __repr__(self):
435- self._update()
436- return str(self.template)
437-
438-
439-def _get_device_type_from_user():
440- """Makes the user write what kind of device this is.
441-
442- If something goes wrong, raises CommandError.
443- """
444- try:
445- dev_type = raw_input("Please specify the device type: ").strip()
446- except (EOFError, KeyboardInterrupt):
447- dev_type = None
448- if not dev_type:
449- raise CommandError("DEVICE name not specified or not correct.")
450- return dev_type
451-
452
453 def get_known_device(name):
454 """Tries to match a device name with a known device type.
455@@ -105,20 +85,9 @@
456 :param name: The name of the device we want matched to a real device.
457 :return A Device instance.
458 """
459- instance = None
460+ instance = Device(DEFAULT_TEMPLATE, name)
461 for known_dev, (matcher, dev_template) in KNOWN_DEVICES.iteritems():
462 if matcher.match(name):
463- instance = Device(name, dev_template)
464- if not instance:
465- dev_type = _get_device_type_from_user()
466- known_dev = KNOWN_DEVICES.get(dev_type, None)
467- if known_dev:
468- instance = Device(name, known_dev[1])
469- else:
470- print ("Device '{0}' does not match a known "
471- "device.".format(dev_type))
472- instance = Device(name, DEFAULT_TEMPLATE)
473- # Not stricly necessary, users can fill up the field later.
474- instance.device_type = dev_type
475-
476+ instance = Device(dev_template, name)
477+ break
478 return instance
479
480=== modified file 'lava/device/commands.py'
481--- lava/device/commands.py 2013-06-27 12:48:26 +0000
482+++ lava/device/commands.py 2013-06-27 12:48:26 +0000
483@@ -56,7 +56,7 @@
484 def invoke(self):
485 real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX])
486
487- if get_device_file(real_file_name):
488+ if get_device_file(real_file_name) is not None:
489 print >> sys.stdout, ("A device configuration file named '{0}' "
490 "already exists.".format(real_file_name))
491 print >> sys.stdout, ("Use 'lava device config {0}' to edit "
492@@ -68,6 +68,7 @@
493 real_file_name))
494
495 device = get_known_device(self.args.DEVICE)
496+ device.update(self.config)
497 device.write(device_conf_file)
498
499 print >> sys.stdout, ("Created device file '{0}' in: {1}".format(
500@@ -116,5 +117,4 @@
501 if device_conf and self.can_edit_file(device_conf):
502 self.edit_file(device_conf)
503 else:
504- raise CommandError("Cannot edit file '{0}' at: "
505- "{1}.".format(real_file_name, device_conf))
506+ raise CommandError("Cannot edit file '{0}'".format(real_file_name))
507
508=== modified file 'lava/device/templates.py'
509--- lava/device/templates.py 2013-06-27 12:48:26 +0000
510+++ lava/device/templates.py 2013-06-27 12:48:26 +0000
511@@ -21,22 +21,50 @@
512 will be used to serialize a Device object.
513 """
514
515+from copy import copy
516+
517+from lava.parameter import Parameter
518+
519+# The hostname parameter is always in the DEFAULT config section.
520+HOSTNAME_PARAMETER = Parameter("hostname")
521+DEVICE_TYPE_PARAMETER = Parameter("device_type", depends=HOSTNAME_PARAMETER)
522+CONNECTION_COMMAND_PARMAETER = Parameter("connection_command",
523+ depends=DEVICE_TYPE_PARAMETER)
524+
525 DEFAULT_TEMPLATE = {
526- 'device_type': None,
527- 'hostname': None,
528- 'connection_command': None,
529+ 'hostname': HOSTNAME_PARAMETER,
530+ 'device_type': DEVICE_TYPE_PARAMETER,
531+ 'connection_command': CONNECTION_COMMAND_PARMAETER,
532 }
533
534+# Specialized copies of the parameters.
535+# We need this or we might end up asking the user twice the same parameter due
536+# to different object references when one Parameter depends on a "specialized"
537+# one, different from the defaults.
538+PANDA_DEVICE_TYPE = copy(DEVICE_TYPE_PARAMETER)
539+PANDA_DEVICE_TYPE.value = "panda"
540+PANDA_DEVICE_TYPE.asked = True
541+
542+PANDA_CONNECTION_COMMAND = copy(CONNECTION_COMMAND_PARMAETER)
543+PANDA_CONNECTION_COMMAND.depends = PANDA_DEVICE_TYPE
544+
545+VEXPRESS_DEVICE_TYPE = copy(DEVICE_TYPE_PARAMETER)
546+VEXPRESS_DEVICE_TYPE.value = "vexpress"
547+VEXPRESS_DEVICE_TYPE.asked = True
548+
549+VEXPRESS_CONNECTION_COMMAND = copy(CONNECTION_COMMAND_PARMAETER)
550+VEXPRESS_CONNECTION_COMMAND.depends = VEXPRESS_DEVICE_TYPE
551+
552 # Dictionary with templates of known devices.
553 KNOWN_TEMPLATES = {
554 'panda': {
555- 'device_type': 'panda',
556- 'hostname': None,
557- 'connection_command': None,
558+ 'hostname': HOSTNAME_PARAMETER,
559+ 'device_type': PANDA_DEVICE_TYPE,
560+ 'connection_command': PANDA_CONNECTION_COMMAND,
561 },
562 'vexpress': {
563- 'device_type': 'vexpress',
564- 'hostname': None,
565- 'connection_command': None,
566+ 'hostname': HOSTNAME_PARAMETER,
567+ 'device_type': VEXPRESS_DEVICE_TYPE,
568+ 'connection_command': VEXPRESS_CONNECTION_COMMAND,
569 },
570 }
571
572=== modified file 'lava/device/tests/test_commands.py'
573--- lava/device/tests/test_commands.py 2013-06-27 12:48:26 +0000
574+++ lava/device/tests/test_commands.py 2013-06-27 12:48:26 +0000
575@@ -22,7 +22,11 @@
576
577 import os
578
579-from mock import MagicMock, patch
580+from mock import (
581+ MagicMock,
582+ call,
583+ patch,
584+)
585
586 from lava.device.commands import (
587 add,
588@@ -33,12 +37,25 @@
589 from lava.tool.errors import CommandError
590
591
592-class CommandsTest(HelperTest):
593-
594+class AddCommandTest(HelperTest):
595+
596+ def test_register_argument(self):
597+ # Make sure that the parser add_argument is called and we have the
598+ # correct argument.
599+ add_command = add(self.parser, self.args)
600+ add_command.register_arguments(self.parser)
601+ name, args, kwargs = self.parser.method_calls[0]
602+ self.assertIn("--non-interactive", args)
603+
604+ name, args, kwargs = self.parser.method_calls[1]
605+ self.assertIn("DEVICE", args)
606+
607+ @patch("lava.device.Device.__str__", new=MagicMock(return_value=""))
608+ @patch("lava.device.Device.update", new=MagicMock())
609 @patch("lava.device.commands.get_device_file",
610 new=MagicMock(return_value=None))
611 @patch("lava.device.commands.get_devices_path")
612- def test_add_invoke(self, get_devices_path_mock):
613+ def test_add_invoke_0(self, get_devices_path_mock):
614 # Tests invocation of the add command. Verifies that the conf file is
615 # written to disk.
616 get_devices_path_mock.return_value = self.temp_dir
617@@ -51,6 +68,37 @@
618 ".".join([self.device, "conf"]))
619 self.assertTrue(os.path.isfile(expected_path))
620
621+ @patch("lava.device.commands.get_known_device")
622+ @patch("lava.device.commands.get_devices_path")
623+ @patch("lava.device.commands.sys.exit")
624+ @patch("lava.device.commands.get_device_file")
625+ def test_add_invoke_1(self, mocked_get_device_file, mocked_sys_exit,
626+ mocked_get_devices_path, mocked_get_known_device):
627+ mocked_get_devices_path.return_value = self.temp_dir
628+ mocked_get_device_file.return_value = self.temp_file.name
629+
630+ add_command = add(self.parser, self.args)
631+ add_command.edit_file = MagicMock()
632+ add_command.invoke()
633+
634+ self.assertTrue(mocked_sys_exit.called)
635+
636+
637+class RemoveCommandTests(HelperTest):
638+
639+ def test_register_argument(self):
640+ # Make sure that the parser add_argument is called and we have the
641+ # correct argument.
642+ command = remove(self.parser, self.args)
643+ command.register_arguments(self.parser)
644+ name, args, kwargs = self.parser.method_calls[0]
645+ self.assertIn("--non-interactive", args)
646+
647+ name, args, kwargs = self.parser.method_calls[1]
648+ self.assertIn("DEVICE", args)
649+
650+ @patch("lava.device.Device.__str__", new=MagicMock(return_value=""))
651+ @patch("lava.device.Device.update", new=MagicMock())
652 @patch("lava.device.commands.get_device_file")
653 @patch("lava.device.commands.get_devices_path")
654 def test_remove_invoke(self, get_devices_path_mock, get_device_file_mock):
655@@ -83,6 +131,33 @@
656 remove_command = remove(self.parser, self.args)
657 self.assertRaises(CommandError, remove_command.invoke)
658
659+
660+class ConfigCommanTests(HelperTest):
661+
662+ def test_register_argument(self):
663+ # Make sure that the parser add_argument is called and we have the
664+ # correct argument.
665+ command = config(self.parser, self.args)
666+ command.register_arguments(self.parser)
667+ name, args, kwargs = self.parser.method_calls[0]
668+ self.assertIn("--non-interactive", args)
669+
670+ name, args, kwargs = self.parser.method_calls[1]
671+ self.assertIn("DEVICE", args)
672+
673+ @patch("lava.device.commands.get_device_file")
674+ def test_config_invoke_0(self, mocked_get_device_file):
675+ command = config(self.parser, self.args)
676+
677+ mocked_get_device_file.return_value = self.temp_file.name
678+ command.can_edit_file = MagicMock(return_value=True)
679+ command.edit_file = MagicMock()
680+ command.invoke()
681+
682+ self.assertTrue(command.edit_file.called)
683+ self.assertEqual([call(self.temp_file.name)],
684+ command.edit_file.call_args_list)
685+
686 @patch("lava.device.commands.get_device_file",
687 new=MagicMock(return_value=None))
688 def test_config_invoke_raises_0(self):
689
690=== modified file 'lava/device/tests/test_device.py'
691--- lava/device/tests/test_device.py 2013-06-27 12:48:26 +0000
692+++ lava/device/tests/test_device.py 2013-06-27 12:48:26 +0000
693@@ -20,10 +20,13 @@
694 Device class unit tests.
695 """
696
697-import sys
698-
699-from StringIO import StringIO
700-
701+from lava.parameter import Parameter
702+from lava.device.templates import (
703+ HOSTNAME_PARAMETER,
704+ PANDA_DEVICE_TYPE,
705+ PANDA_CONNECTION_COMMAND,
706+)
707+from lava.tests.test_config import MockedConfig
708 from lava.device import (
709 Device,
710 get_known_device,
711@@ -38,70 +41,73 @@
712 # User creates a new device with a guessable name for a device.
713 instance = get_known_device('panda_new_01')
714 self.assertIsInstance(instance, Device)
715- self.assertEqual(instance.template['device_type'], 'panda')
716- self.assertIsNone(instance.device_type)
717+ self.assertEqual(instance.data['device_type'].value, 'panda')
718
719 def test_get_known_device_panda_1(self):
720 # User creates a new device with a guessable name for a device.
721 # Name passed has capital letters.
722 instance = get_known_device('new_PanDa_02')
723 self.assertIsInstance(instance, Device)
724- self.assertEqual(instance.template['device_type'], 'panda')
725- self.assertIsNone(instance.device_type)
726+ self.assertEqual(instance.data['device_type'].value, 'panda')
727
728 def test_get_known_device_vexpress_0(self):
729 # User creates a new device with a guessable name for a device.
730 # Name passed has capital letters.
731 instance = get_known_device('a_VexPress_Device')
732 self.assertIsInstance(instance, Device)
733- self.assertEqual(instance.template['device_type'], 'vexpress')
734- self.assertIsNone(instance.device_type)
735+ self.assertEqual(instance.data['device_type'].value, 'vexpress')
736
737 def test_get_known_device_vexpress_1(self):
738 # User creates a new device with a guessable name for a device.
739 instance = get_known_device('another-vexpress')
740 self.assertIsInstance(instance, Device)
741- self.assertEqual(instance.template['device_type'], 'vexpress')
742- self.assertIsNone(instance.device_type)
743-
744- def test_instance_update(self):
745- # Tests that when calling the _update() function with an known device
746- # it does not update the device_type instance attribute, and that the
747- # template contains the correct name.
748- instance = get_known_device('Another_PanDa_device')
749- instance._update()
750- self.assertIsInstance(instance, Device)
751- self.assertEqual(instance.template['device_type'], 'panda')
752- self.assertIsNone(instance.device_type)
753-
754- def test_get_known_device_unknown(self):
755- # User tries to create a new device with an unknown device type. She
756- # is asked to insert the device type and types 'a_fake_device'.
757- sys.stdin = StringIO('a_fake_device')
758- instance = get_known_device('a_fake_device')
759- self.assertIsInstance(instance, Device)
760- self.assertEqual(instance.device_type, 'a_fake_device')
761-
762- def test_get_known_device_known(self):
763- # User tries to create a new device with a not recognizable name.
764- # She is asked to insert the device type and types 'panda'.
765- sys.stdin = StringIO("panda")
766- instance = get_known_device("another_fake_device")
767- self.assertIsInstance(instance, Device)
768- self.assertEqual(instance.template["device_type"], "panda")
769-
770- def test_get_known_device_raises(self):
771- # User tries to create a new device, but in some way nothing is passed
772- # on the command line when asked.
773- self.assertRaises(CommandError, get_known_device, 'a_fake_device')
774+ self.assertIsInstance(instance.data['device_type'], Parameter)
775+ self.assertEqual(instance.data['device_type'].value, 'vexpress')
776+
777+ def test_device_update_1(self):
778+ # Tests that when calling update() on a Device, the template gets
779+ # updated with the correct values from a Config instance.
780+ hostname = "panda_device"
781+
782+ config = MockedConfig(self.temp_file.name)
783+ config.put_parameter(HOSTNAME_PARAMETER, hostname)
784+ config.put_parameter(PANDA_DEVICE_TYPE, "panda")
785+ config.put_parameter(PANDA_CONNECTION_COMMAND, "test")
786+
787+ expected = {
788+ "hostname": hostname,
789+ "device_type": "panda",
790+ "connection_command": "test"
791+ }
792+
793+ instance = get_known_device(hostname)
794+ instance.update(config)
795+
796+ self.assertEqual(expected, instance.data)
797
798 def test_device_write(self):
799 # User tries to create a new panda device. The conf file is written
800 # and contains the expected results.
801- expected = ("hostname = panda02\nconnection_command = \n"
802+ hostname = "panda_device"
803+
804+ config = MockedConfig(self.temp_file.name)
805+ config.put_parameter(HOSTNAME_PARAMETER, hostname)
806+ config.put_parameter(PANDA_DEVICE_TYPE, "panda")
807+ config.put_parameter(PANDA_CONNECTION_COMMAND, "test")
808+
809+ expected = {
810+ "hostname": hostname,
811+ "device_type": "panda",
812+ "connection_command": "test"
813+ }
814+
815+ instance = get_known_device(hostname)
816+ instance.update(config)
817+ instance.write(self.temp_file.name)
818+
819+ expected = ("hostname = panda_device\nconnection_command = test\n"
820 "device_type = panda\n")
821- instance = get_known_device("panda02")
822- instance.write(self.temp_file.name)
823+
824 obtained = ""
825 with open(self.temp_file.name) as f:
826 obtained = f.read()
827
828=== modified file 'lava/helper/command.py'
829--- lava/helper/command.py 2013-06-27 12:48:26 +0000
830+++ lava/helper/command.py 2013-06-27 12:48:26 +0000
831@@ -93,7 +93,7 @@
832 :return The command execution return code.
833 """
834 if not isinstance(cmd_args, list):
835- cmd_args = list(cmd_args)
836+ cmd_args = [cmd_args]
837 try:
838 return subprocess.check_call(cmd_args)
839 except subprocess.CalledProcessError:
840
841=== added file 'lava/helper/template.py'
842--- lava/helper/template.py 1970-01-01 00:00:00 +0000
843+++ lava/helper/template.py 2013-06-27 12:48:26 +0000
844@@ -0,0 +1,44 @@
845+# Copyright (C) 2013 Linaro Limited
846+#
847+# Author: Milo Casagrande <milo.casagrande@linaro.org>
848+#
849+# This file is part of lava-tool.
850+#
851+# lava-tool is free software: you can redistribute it and/or modify
852+# it under the terms of the GNU Lesser General Public License version 3
853+# as published by the Free Software Foundation
854+#
855+# lava-tool is distributed in the hope that it will be useful,
856+# but WITHOUT ANY WARRANTY; without even the implied warranty of
857+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
858+# GNU General Public License for more details.
859+#
860+# You should have received a copy of the GNU Lesser General Public License
861+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
862+
863+from lava.parameter import Parameter
864+
865+
866+def expand_template(template, config):
867+ """Updates a template based on the values from the provided config.
868+
869+ :param template: A template to be updated.
870+ :param config: A Config instance where values should be taken.
871+ """
872+
873+ def update(data):
874+ """Internal recursive function."""
875+ if isinstance(data, dict):
876+ keys = data.keys()
877+ elif isinstance(data, list):
878+ keys = range(len(data))
879+ else:
880+ return
881+ for key in keys:
882+ entry = data[key]
883+ if isinstance(entry, Parameter):
884+ data[key] = config.get(entry)
885+ else:
886+ update(entry)
887+
888+ update(template)
889
890=== modified file 'lava/helper/tests/test_command.py'
891--- lava/helper/tests/test_command.py 2013-06-27 12:48:26 +0000
892+++ lava/helper/tests/test_command.py 2013-06-27 12:48:26 +0000
893@@ -18,6 +18,14 @@
894
895 """lava.herlp.command module tests."""
896
897+import subprocess
898+from mock import (
899+ MagicMock,
900+ call,
901+ patch,
902+)
903+
904+from lava.tool.errors import CommandError
905 from lava.helper.command import BaseCommand
906 from lava.helper.tests.helper_test import HelperTest
907
908@@ -51,3 +59,65 @@
909 obtained = f.read()
910
911 self.assertEqual(expected, obtained)
912+
913+ @patch("lava.helper.command.subprocess")
914+ def test_run_0(self, mocked_subprocess):
915+ mocked_subprocess.check_call = MagicMock()
916+ BaseCommand.run("foo")
917+ self.assertEqual(mocked_subprocess.check_call.call_args_list,
918+ [call(["foo"])])
919+ self.assertTrue(mocked_subprocess.check_call.called)
920+
921+ @patch("lava.helper.command.subprocess.check_call")
922+ def test_run_1(self, mocked_check_call):
923+ mocked_check_call.side_effect = subprocess.CalledProcessError(1, "foo")
924+ self.assertRaises(CommandError, BaseCommand.run, ["foo"])
925+
926+ @patch("lava.helper.command.subprocess")
927+ @patch("lava.helper.command.has_command", return_value=False)
928+ @patch("lava.helper.command.os.environ.get", return_value=None)
929+ @patch("lava.helper.command.sys.exit")
930+ def test_edit_file_0(self, mocked_sys_exit, mocked_env_get,
931+ mocked_has_command, mocked_subprocess):
932+ BaseCommand.edit_file(self.temp_file.name)
933+ self.assertTrue(mocked_sys_exit.called)
934+
935+ @patch("lava.helper.command.subprocess")
936+ @patch("lava.helper.command.has_command", side_effect=[True, False])
937+ @patch("lava.helper.command.os.environ.get", return_value=None)
938+ def test_edit_file_1(self, mocked_env_get, mocked_has_command,
939+ mocked_subprocess):
940+ mocked_subprocess.Popen = MagicMock()
941+ BaseCommand.edit_file(self.temp_file.name)
942+ expected = [call(["sensible-editor", self.temp_file.name])]
943+ self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
944+
945+ @patch("lava.helper.command.subprocess")
946+ @patch("lava.helper.command.has_command", side_effect=[False, True])
947+ @patch("lava.helper.command.os.environ.get", return_value=None)
948+ def test_edit_file_2(self, mocked_env_get, mocked_has_command,
949+ mocked_subprocess):
950+ mocked_subprocess.Popen = MagicMock()
951+ BaseCommand.edit_file(self.temp_file.name)
952+ expected = [call(["xdg-open", self.temp_file.name])]
953+ self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
954+
955+ @patch("lava.helper.command.subprocess")
956+ @patch("lava.helper.command.has_command", return_value=False)
957+ @patch("lava.helper.command.os.environ.get", return_value="vim")
958+ def test_edit_file_3(self, mocked_env_get, mocked_has_command,
959+ mocked_subprocess):
960+ mocked_subprocess.Popen = MagicMock()
961+ BaseCommand.edit_file(self.temp_file.name)
962+ expected = [call(["vim", self.temp_file.name])]
963+ self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
964+
965+ @patch("lava.helper.command.subprocess")
966+ @patch("lava.helper.command.has_command", return_value=False)
967+ @patch("lava.helper.command.os.environ.get", return_value="vim")
968+ def test_edit_file_4(self, mocked_env_get, mocked_has_command,
969+ mocked_subprocess):
970+ mocked_subprocess.Popen = MagicMock()
971+ mocked_subprocess.Popen.side_effect = Exception()
972+ self.assertRaises(CommandError, BaseCommand.edit_file,
973+ self.temp_file.name)
974
975=== modified file 'lava/helper/tests/test_dispatcher.py'
976--- lava/helper/tests/test_dispatcher.py 2013-06-27 12:48:26 +0000
977+++ lava/helper/tests/test_dispatcher.py 2013-06-27 12:48:26 +0000
978@@ -19,10 +19,12 @@
979 """lava.helper.dispatcher tests."""
980
981 import os
982+import tempfile
983+
984+from mock import patch
985+
986 from lava.tool.errors import CommandError
987-
988 from lava.helper.tests.helper_test import HelperTest
989-
990 from lava.helper.dispatcher import (
991 choose_devices_path,
992 )
993@@ -30,6 +32,15 @@
994
995 class DispatcherTests(HelperTest):
996
997+ def setUp(self):
998+ super(DispatcherTests, self).setUp()
999+ self.devices_dir = os.path.join(tempfile.gettempdir(), "devices")
1000+ os.makedirs(self.devices_dir)
1001+
1002+ def tearDown(self):
1003+ super(DispatcherTests, self).tearDown()
1004+ os.removedirs(self.devices_dir)
1005+
1006 def test_choose_devices_path_0(self):
1007 # Tests that when passing more than one path, the first writable one
1008 # is returned.
1009@@ -50,3 +61,17 @@
1010 obtained = choose_devices_path([self.temp_dir])
1011 self.assertEqual(expected_path, obtained)
1012 self.assertTrue(os.path.isdir(expected_path))
1013+
1014+ def test_choose_devices_path_3(self):
1015+ # Tests that returns the already existing devices path.
1016+ obtained = choose_devices_path([tempfile.gettempdir()])
1017+ self.assertEqual(self.devices_dir, obtained)
1018+
1019+ @patch("__builtin__.open")
1020+ def test_choose_devices_path_4(self, mocked_open):
1021+ # Tests that when IOError is raised and we pass only one dir
1022+ # CommandError is raised.
1023+ mocked_open.side_effect = IOError()
1024+ self.assertRaises(CommandError, choose_devices_path,
1025+ [tempfile.gettempdir()])
1026+ self.assertTrue(mocked_open.called)
1027
1028=== modified file 'lava/job/__init__.py'
1029--- lava/job/__init__.py 2013-06-27 12:48:26 +0000
1030+++ lava/job/__init__.py 2013-06-27 12:48:26 +0000
1031@@ -19,7 +19,7 @@
1032 from copy import deepcopy
1033 import json
1034
1035-from lava.job.templates import Parameter
1036+from lava.helper.template import expand_template
1037
1038
1039 class Job:
1040@@ -27,21 +27,7 @@
1041 self.data = deepcopy(template)
1042
1043 def fill_in(self, config):
1044-
1045- def insert_data(data):
1046- if isinstance(data, dict):
1047- keys = data.keys()
1048- elif isinstance(data, list):
1049- keys = range(len(data))
1050- else:
1051- return
1052- for key in keys:
1053- entry = data[key]
1054- if isinstance(entry, Parameter):
1055- data[key] = config.get(entry)
1056- else:
1057- insert_data(entry)
1058- insert_data(self.data)
1059+ expand_template(self.data, config)
1060
1061 def write(self, stream):
1062 stream.write(json.dumps(self.data, indent=4))
1063
1064=== modified file 'lava/job/commands.py'
1065--- lava/job/commands.py 2013-06-27 12:48:26 +0000
1066+++ lava/job/commands.py 2013-06-27 12:48:26 +0000
1067@@ -25,12 +25,13 @@
1068 import xmlrpclib
1069
1070 from lava.helper.command import BaseCommand
1071+from lava.helper.dispatcher import get_devices
1072
1073-from lava.config import Parameter
1074 from lava.job import Job
1075 from lava.job.templates import (
1076 BOOT_TEST,
1077 )
1078+from lava.parameter import Parameter
1079 from lava.tool.command import CommandGroup
1080 from lava.tool.errors import CommandError
1081 from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend
1082@@ -126,7 +127,7 @@
1083 def invoke(self):
1084 if os.path.isfile(self.args.FILE):
1085 if has_command("lava-dispatch"):
1086- devices = self.get_devices()
1087+ devices = get_devices()
1088 if devices:
1089 if len(devices) > 1:
1090 device = self._choose_device(devices)
1091
1092=== modified file 'lava/job/templates.py'
1093--- lava/job/templates.py 2013-06-27 12:48:26 +0000
1094+++ lava/job/templates.py 2013-06-27 12:48:26 +0000
1095@@ -16,7 +16,7 @@
1096 # You should have received a copy of the GNU Lesser General Public License
1097 # along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
1098
1099-from lava.config import Parameter
1100+from lava.parameter import Parameter
1101
1102 device_type = Parameter("device_type")
1103 prebuilt_image = Parameter("prebuilt_image", depends=device_type)
1104
1105=== modified file 'lava/job/tests/test_commands.py'
1106--- lava/job/tests/test_commands.py 2013-06-27 12:48:26 +0000
1107+++ lava/job/tests/test_commands.py 2013-06-27 12:48:26 +0000
1108@@ -25,15 +25,14 @@
1109
1110 from mock import MagicMock, patch
1111
1112-from lava.config import NonInteractiveConfig, Parameter
1113-
1114+from lava.config import Config
1115+from lava.helper.tests.helper_test import HelperTest
1116 from lava.job.commands import (
1117 new,
1118 run,
1119 submit,
1120 )
1121-
1122-from lava.helper.tests.helper_test import HelperTest
1123+from lava.parameter import Parameter
1124 from lava.tool.errors import CommandError
1125
1126
1127@@ -46,8 +45,9 @@
1128 self.device_type = Parameter('device_type')
1129 self.prebuilt_image = Parameter('prebuilt_image',
1130 depends=self.device_type)
1131- self.config = NonInteractiveConfig(
1132- {'device_type': 'foo', 'prebuilt_image': 'bar'})
1133+ self.config = Config()
1134+ self.config.put_parameter(self.device_type, 'foo')
1135+ self.config.put_parameter(self.prebuilt_image, 'bar')
1136
1137 def tmp(self, filename):
1138 """Returns a path to a non existent file.
1139
1140=== modified file 'lava/job/tests/test_job.py'
1141--- lava/job/tests/test_job.py 2013-05-28 22:08:12 +0000
1142+++ lava/job/tests/test_job.py 2013-06-27 12:48:26 +0000
1143@@ -21,15 +21,29 @@
1144 """
1145
1146 import json
1147+import os
1148+import tempfile
1149+
1150+from StringIO import StringIO
1151 from unittest import TestCase
1152-from StringIO import StringIO
1153
1154-from lava.config import NonInteractiveConfig
1155-from lava.job.templates import *
1156+from lava.config import Config
1157 from lava.job import Job
1158+from lava.job.templates import BOOT_TEST
1159+from lava.parameter import Parameter
1160+
1161
1162 class JobTest(TestCase):
1163
1164+ def setUp(self):
1165+ self.config_file = tempfile.NamedTemporaryFile(delete=False)
1166+ self.config = Config()
1167+ self.config._config_file = self.config_file.name
1168+
1169+ def tearDown(self):
1170+ if os.path.isfile(self.config_file.name):
1171+ os.unlink(self.config_file.name)
1172+
1173 def test_from_template(self):
1174 template = {}
1175 job = Job(template)
1176@@ -37,21 +51,20 @@
1177 self.assertIsNot(job.data, template)
1178
1179 def test_fill_in_data(self):
1180+ image = "/path/to/panda.img"
1181+ param1 = Parameter("device_type")
1182+ param2 = Parameter("prebuilt_image", depends=param1)
1183+ self.config.put_parameter(param1, "panda")
1184+ self.config.put_parameter(param2, image)
1185+
1186 job = Job(BOOT_TEST)
1187- image = "/path/to/panda.img"
1188- config = NonInteractiveConfig(
1189- {
1190- "device_type": "panda",
1191- "prebuilt_image": image,
1192- }
1193- )
1194- job.fill_in(config)
1195+ job.fill_in(self.config)
1196
1197 self.assertEqual(job.data['device_type'], "panda")
1198 self.assertEqual(job.data['actions'][0]["parameters"]["image"], image)
1199
1200 def test_write(self):
1201- orig_data = { "foo": "bar" }
1202+ orig_data = {"foo": "bar"}
1203 job = Job(orig_data)
1204 output = StringIO()
1205 job.write(output)
1206@@ -60,7 +73,7 @@
1207 self.assertEqual(data, orig_data)
1208
1209 def test_writes_nicely_formatted_json(self):
1210- orig_data = { "foo": "bar" }
1211+ orig_data = {"foo": "bar"}
1212 job = Job(orig_data)
1213 output = StringIO()
1214 job.write(output)
1215
1216=== added file 'lava/parameter.py'
1217--- lava/parameter.py 1970-01-01 00:00:00 +0000
1218+++ lava/parameter.py 2013-06-27 12:48:26 +0000
1219@@ -0,0 +1,75 @@
1220+# Copyright (C) 2013 Linaro Limited
1221+#
1222+# Author: Antonio Terceiro <antonio.terceiro@linaro.org>
1223+#
1224+# This file is part of lava-tool.
1225+#
1226+# lava-tool is free software: you can redistribute it and/or modify
1227+# it under the terms of the GNU Lesser General Public License version 3
1228+# as published by the Free Software Foundation
1229+#
1230+# lava-tool is distributed in the hope that it will be useful,
1231+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1232+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1233+# GNU General Public License for more details.
1234+#
1235+# You should have received a copy of the GNU Lesser General Public License
1236+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
1237+
1238+"""
1239+Parameter class and its accessory methods/functions.
1240+"""
1241+
1242+import sys
1243+
1244+
1245+class Parameter(object):
1246+ """A parameter with an optional dependency."""
1247+ def __init__(self, id, value=None, depends=None):
1248+ """Creates a new parameter.
1249+
1250+ :param id: The name of this parameter.
1251+ :param value: The value of this parameter. Defaults to None.
1252+ :param depends: If this Parameter depends on another one. Defaults
1253+ to None.
1254+ :type Parameter
1255+ """
1256+ self.id = id
1257+ self.value = value
1258+ self.depends = depends
1259+ self.asked = False
1260+
1261+ def prompt(self, old_value=None):
1262+ """Gets the parameter value from the user.
1263+
1264+ To get user input, the builtin `raw_input` function will be used. Input
1265+ will also be stripped of possible whitespace chars. If Enter or any
1266+ sort of whitespace chars in typed, the old Parameter value will be
1267+ returned.
1268+
1269+ :param old_value: The old parameter value.
1270+ :return The input as typed by the user, or the old value.
1271+ """
1272+ if old_value is not None:
1273+ prompt = "{0} [{1}]: ".format(self.id, old_value)
1274+ else:
1275+ prompt = "{0}: ".format(self.id)
1276+
1277+ user_input = None
1278+ try:
1279+ user_input = raw_input(prompt).strip()
1280+ except EOFError:
1281+ pass
1282+ except KeyboardInterrupt:
1283+ sys.exit(-1)
1284+
1285+ if user_input is not None:
1286+ if len(user_input) == 0 and old_value:
1287+ # Keep the old value when user press enter or another
1288+ # whitespace char.
1289+ self.value = old_value
1290+ else:
1291+ self.value = user_input
1292+
1293+ self.asked = True
1294+ return self.value
1295
1296=== added directory 'lava/tests'
1297=== added file 'lava/tests/__init__.py'
1298=== added file 'lava/tests/test_config.py'
1299--- lava/tests/test_config.py 1970-01-01 00:00:00 +0000
1300+++ lava/tests/test_config.py 2013-06-27 12:48:26 +0000
1301@@ -0,0 +1,278 @@
1302+# Copyright (C) 2013 Linaro Limited
1303+#
1304+# Author: Milo Casagrande <milo.casagrande@linaro.org>
1305+#
1306+# This file is part of lava-tool.
1307+#
1308+# lava-tool is free software: you can redistribute it and/or modify
1309+# it under the terms of the GNU Lesser General Public License version 3
1310+# as published by the Free Software Foundation
1311+#
1312+# lava-tool is distributed in the hope that it will be useful,
1313+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1314+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1315+# GNU General Public License for more details.
1316+#
1317+# You should have received a copy of the GNU Lesser General Public License
1318+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
1319+
1320+"""
1321+lava.config unit tests.
1322+"""
1323+
1324+import os
1325+import sys
1326+import tempfile
1327+
1328+from StringIO import StringIO
1329+from mock import MagicMock, patch, call
1330+
1331+from lava.config import (
1332+ Config,
1333+ InteractiveConfig,
1334+ ConfigParser,
1335+)
1336+from lava.helper.tests.helper_test import HelperTest
1337+from lava.parameter import Parameter
1338+from lava.tool.errors import CommandError
1339+
1340+
1341+class MockedConfig(Config):
1342+ """A subclass of the original Config class.
1343+
1344+ Used to test the Config class, but to not have the same constructor in
1345+ order to use temporary files for the configuration.
1346+ """
1347+ def __init__(self, config_file):
1348+ self._cache = {}
1349+ self._config_file = config_file
1350+ self._config_backend = ConfigParser()
1351+ self._config_backend.read([self._config_file])
1352+
1353+
1354+class MockedInteractiveConfig(InteractiveConfig):
1355+ def __init__(self, config_file, force_interactive=False):
1356+ self._cache = {}
1357+ self._config_file = config_file
1358+ self._config_backend = ConfigParser()
1359+ self._config_backend.read([self._config_file])
1360+ self._force_interactive = force_interactive
1361+
1362+
1363+class ConfigTestCase(HelperTest):
1364+ """General test case class for the different Config classes."""
1365+ def setUp(self):
1366+ super(ConfigTestCase, self).setUp()
1367+ self.config_file = tempfile.NamedTemporaryFile(delete=False)
1368+
1369+ self.param1 = Parameter("foo")
1370+ self.param2 = Parameter("bar", depends=self.param1)
1371+
1372+ def tearDown(self):
1373+ super(ConfigTestCase, self).tearDown()
1374+ if os.path.isfile(self.config_file.name):
1375+ os.unlink(self.config_file.name)
1376+
1377+
1378+class ConfigTest(ConfigTestCase):
1379+
1380+ def setUp(self):
1381+ super(ConfigTest, self).setUp()
1382+ self.config = MockedConfig(self.config_file.name)
1383+
1384+ def test_assert_temp_config_file(self):
1385+ # Dummy test to make sure we are overriding correctly the Config class.
1386+ self.assertEqual(self.config._config_file, self.config_file.name)
1387+
1388+ def test_config_put_in_cache_0(self):
1389+ self.config._put_in_cache("key", "value", "section")
1390+ self.assertEqual(self.config._cache["section"]["key"], "value")
1391+
1392+ def test_config_get_from_cache_0(self):
1393+ self.config._put_in_cache("key", "value", "section")
1394+ obtained = self.config._get_from_cache(Parameter("key"), "section")
1395+ self.assertEqual("value", obtained)
1396+
1397+ def test_config_get_from_cache_1(self):
1398+ self.config._put_in_cache("key", "value", "DEFAULT")
1399+ obtained = self.config._get_from_cache(Parameter("key"), "DEFAULT")
1400+ self.assertEqual("value", obtained)
1401+
1402+ def test_config_put_0(self):
1403+ # Puts a value in the DEFAULT section.
1404+ self.config._put_in_cache = MagicMock()
1405+ self.config.put("foo", "foo")
1406+ expected = "foo"
1407+ obtained = self.config._config_backend.get("DEFAULT", "foo")
1408+ self.assertEqual(expected, obtained)
1409+
1410+ def test_config_put_1(self):
1411+ # Puts a value in a new section.
1412+ self.config._put_in_cache = MagicMock()
1413+ self.config.put("foo", "foo", "bar")
1414+ expected = "foo"
1415+ obtained = self.config._config_backend.get("bar", "foo")
1416+ self.assertEqual(expected, obtained)
1417+
1418+ def test_config_put_parameter_0(self):
1419+ self.config._calculate_config_section = MagicMock(return_value="")
1420+ self.assertRaises(CommandError, self.config.put_parameter, self.param1)
1421+
1422+ @patch("lava.config.Config.put")
1423+ def test_config_put_parameter_1(self, mocked_config_put):
1424+ self.config._calculate_config_section = MagicMock(
1425+ return_value="DEFAULT")
1426+
1427+ self.param1.value = "bar"
1428+ self.config.put_parameter(self.param1)
1429+
1430+ self.assertEqual(mocked_config_put.mock_calls,
1431+ [call("foo", "bar", "DEFAULT")])
1432+
1433+ def test_config_get_0(self):
1434+ # Tests that with a non existing parameter, it returns None.
1435+ param = Parameter("baz")
1436+ self.config._get_from_cache = MagicMock(return_value=None)
1437+ self.config._calculate_config_section = MagicMock(
1438+ return_value="DEFAULT")
1439+
1440+ expected = None
1441+ obtained = self.config.get(param)
1442+ self.assertEqual(expected, obtained)
1443+
1444+ def test_config_get_1(self):
1445+ self.config.put_parameter(self.param1, "foo")
1446+ self.config._get_from_cache = MagicMock(return_value=None)
1447+ self.config._calculate_config_section = MagicMock(
1448+ return_value="DEFAULT")
1449+
1450+ expected = "foo"
1451+ obtained = self.config.get(self.param1)
1452+ self.assertEqual(expected, obtained)
1453+
1454+ def test_calculate_config_section_0(self):
1455+ expected = "DEFAULT"
1456+ obtained = self.config._calculate_config_section(self.param1)
1457+ self.assertEqual(expected, obtained)
1458+
1459+ def test_calculate_config_section_1(self):
1460+ self.config.put_parameter(self.param1, "foo")
1461+ expected = "foo=foo"
1462+ obtained = self.config._calculate_config_section(self.param2)
1463+ self.assertEqual(expected, obtained)
1464+
1465+ def test_config_save(self):
1466+ self.config.put_parameter(self.param1, "foo")
1467+ self.config.save()
1468+
1469+ expected = "[DEFAULT]\nfoo = foo\n\n"
1470+ obtained = ""
1471+ with open(self.config_file.name) as tmp_file:
1472+ obtained = tmp_file.read()
1473+ self.assertEqual(expected, obtained)
1474+
1475+ @patch("lava.config.AT_EXIT_CALLS", spec=set)
1476+ def test_config_atexit_call_list(self, mocked_calls):
1477+ # Tests that the save() method is added to the set of atexit calls.
1478+ config = Config()
1479+ config._config_file = self.config_file.name
1480+ config.put_parameter(self.param1, "foo")
1481+
1482+ expected = [call.add(config.save)]
1483+
1484+ self.assertEqual(expected, mocked_calls.mock_calls)
1485+
1486+
1487+class InteractiveConfigTest(ConfigTestCase):
1488+
1489+ def setUp(self):
1490+ super(InteractiveConfigTest, self).setUp()
1491+ self.config = MockedInteractiveConfig(
1492+ config_file=self.config_file.name)
1493+
1494+ @patch("lava.config.Config.get", new=MagicMock(return_value=None))
1495+ def test_non_interactive_config_0(self):
1496+ # Mocked config default is not to be interactive.
1497+ # Try to get a value that does not exists, users just press enter when
1498+ # asked for a value. Value will be empty.
1499+ sys.stdin = StringIO("\n")
1500+ value = self.config.get(Parameter("foo"))
1501+ self.assertEqual("", value)
1502+
1503+ @patch("lava.config.Config.get", new=MagicMock(return_value="value"))
1504+ def test_non_interactive_config_1(self):
1505+ # Parent class config returns a value, but we are not interactive.
1506+ value = self.config.get(Parameter("foo"))
1507+ self.assertEqual("value", value)
1508+
1509+ @patch("lava.config.Config.get", new=MagicMock(return_value=None))
1510+ def test_non_interactive_config_2(self):
1511+ expected = "bar"
1512+ sys.stdin = StringIO(expected)
1513+ value = self.config.get(Parameter("foo"))
1514+ self.assertEqual(expected, value)
1515+
1516+ @patch("lava.config.Config.get", new=MagicMock(return_value="value"))
1517+ def test_interactive_config_0(self):
1518+ # We force to be interactive, meaning that even if a value is found,
1519+ # it will be asked anyway.
1520+ self.config._force_interactive = True
1521+ expected = "a_new_value"
1522+ sys.stdin = StringIO(expected)
1523+ value = self.config.get(Parameter("foo"))
1524+ self.assertEqual(expected, value)
1525+
1526+ @patch("lava.config.Config.get", new=MagicMock(return_value="value"))
1527+ def test_interactive_config_1(self):
1528+ # Force to be interactive, but when asked for the new value press
1529+ # Enter. The old value should be returned.
1530+ self.config._force_interactive = True
1531+ sys.stdin = StringIO("\n")
1532+ value = self.config.get(Parameter("foo"))
1533+ self.assertEqual("value", value)
1534+
1535+ def test_calculate_config_section_0(self):
1536+ self.config._force_interactive = True
1537+ obtained = self.config._calculate_config_section(self.param1)
1538+ expected = "DEFAULT"
1539+ self.assertEqual(expected, obtained)
1540+
1541+ def test_calculate_config_section_1(self):
1542+ self.param2.depends.asked = True
1543+ self.config._force_interactive = True
1544+ self.config.put(self.param1.id, "foo")
1545+ obtained = self.config._calculate_config_section(self.param2)
1546+ expected = "foo=foo"
1547+ self.assertEqual(expected, obtained)
1548+
1549+ def test_calculate_config_section_2(self):
1550+ self.config._force_interactive = True
1551+ self.config._config_backend.get = MagicMock(return_value=None)
1552+ sys.stdin = StringIO("baz")
1553+ expected = "foo=baz"
1554+ obtained = self.config._calculate_config_section(self.param2)
1555+ self.assertEqual(expected, obtained)
1556+
1557+ def test_calculate_config_section_3(self):
1558+ # Tests that when a parameter has its value in the cache and also on
1559+ # file, we honor the cached version.
1560+ self.param2.depends.asked = True
1561+ self.config._force_interactive = True
1562+ self.config._get_from_cache = MagicMock(return_value="bar")
1563+ self.config._config_backend.get = MagicMock(return_value="baz")
1564+ expected = "foo=bar"
1565+ obtained = self.config._calculate_config_section(self.param2)
1566+ self.assertEqual(expected, obtained)
1567+
1568+ @patch("lava.config.Config.get", new=MagicMock(return_value=None))
1569+ @patch("lava.parameter.sys.exit")
1570+ @patch("lava.parameter.raw_input", create=True)
1571+ def test_interactive_config_exit(self, mocked_raw, mocked_sys_exit):
1572+ self.config._calculate_config_section = MagicMock(
1573+ return_value="DEFAULT")
1574+
1575+ mocked_raw.side_effect = KeyboardInterrupt()
1576+
1577+ self.config._force_interactive = True
1578+ self.config.get(self.param1)
1579+ self.assertTrue(mocked_sys_exit.called)
1580
1581=== added file 'lava/tests/test_parameter.py'
1582--- lava/tests/test_parameter.py 1970-01-01 00:00:00 +0000
1583+++ lava/tests/test_parameter.py 2013-06-27 12:48:26 +0000
1584@@ -0,0 +1,51 @@
1585+# Copyright (C) 2013 Linaro Limited
1586+#
1587+# Author: Milo Casagrande <milo.casagrande@linaro.org>
1588+#
1589+# This file is part of lava-tool.
1590+#
1591+# lava-tool is free software: you can redistribute it and/or modify
1592+# it under the terms of the GNU Lesser General Public License version 3
1593+# as published by the Free Software Foundation
1594+#
1595+# lava-tool is distributed in the hope that it will be useful,
1596+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1597+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1598+# GNU General Public License for more details.
1599+#
1600+# You should have received a copy of the GNU Lesser General Public License
1601+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
1602+
1603+"""
1604+lava.parameter unit tests.
1605+"""
1606+
1607+import sys
1608+
1609+from StringIO import StringIO
1610+from mock import patch
1611+
1612+from lava.helper.tests.helper_test import HelperTest
1613+from lava.parameter import Parameter
1614+
1615+
1616+class ParameterTest(HelperTest):
1617+
1618+ def setUp(self):
1619+ super(ParameterTest, self).setUp()
1620+ self.parameter1 = Parameter("foo", value="baz")
1621+
1622+ def test_prompt_0(self):
1623+ # Tests that when we have a value in the parameters and the user press
1624+ # Enter, we get the old value back.
1625+ sys.stdin = StringIO("\n")
1626+ obtained = self.parameter1.prompt()
1627+ self.assertEqual(self.parameter1.value, obtained)
1628+
1629+ @patch("lava.parameter.raw_input", create=True)
1630+ def test_prompt_1(self, mocked_raw_input):
1631+ # Tests that with a value stored in the parameter, if and EOFError is
1632+ # raised when getting user input, we get back the old value.
1633+ mocked_raw_input.side_effect = EOFError()
1634+ obtained = self.parameter1.prompt()
1635+ self.assertEqual(self.parameter1.value, obtained)
1636
1637=== modified file 'lava_tool/tests/__init__.py'
1638--- lava_tool/tests/__init__.py 2013-06-27 12:48:26 +0000
1639+++ lava_tool/tests/__init__.py 2013-06-27 12:48:26 +0000
1640@@ -44,6 +44,8 @@
1641 'lava.job.tests.test_commands',
1642 'lava.device.tests.test_device',
1643 'lava.device.tests.test_commands',
1644+ 'lava.tests.test_config',
1645+ 'lava.tests.test_parameter',
1646 'lava.helper.tests.test_command',
1647 'lava.helper.tests.test_dispatcher',
1648 ]

Subscribers

People subscribed via source and target branches