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
=== modified file '.bzrignore'
--- .bzrignore 2013-06-27 12:48:26 +0000
+++ .bzrignore 2013-06-27 12:48:26 +0000
@@ -5,3 +5,4 @@
5/tags5/tags
6.testrepository6.testrepository
7*.egg7*.egg
8lava_tool_coverage
89
=== added file '.coveragerc'
--- .coveragerc 1970-01-01 00:00:00 +0000
+++ .coveragerc 2013-06-27 12:48:26 +0000
@@ -0,0 +1,13 @@
1[run]
2branch = True
3source = .
4omit =
5 setup*
6
7[report]
8precision = 2
9show_missing = True
10
11[html]
12title = Code Coverage of lava-tool
13directory = lava_tool_coverage
014
=== added file 'HACKING'
--- HACKING 1970-01-01 00:00:00 +0000
+++ HACKING 2013-06-27 12:48:26 +0000
@@ -0,0 +1,17 @@
1Tests Code Coverage
2===================
3
4To have a nicely HTML viewable report on tests code coverage, do as follows:
5
6* Install `python-coverage` (`pip install coverage` in case you use pip)
7* Run the following command:
8
9 python-coverage run -m unittest lava_tool.tests.test_suite 2>/dev/null && python-coverage html
10
11* The report will be saved in a directory called `lava_tool_coverage`: open
12the `index.html` file in there to see the report.
13
14Notes:
15
16 * To re-run the coverage report, you have to delete the `lava_tool_coverage`
17directory first, otherwise `python-coverage` will fail.
018
=== modified file 'ci-build'
--- ci-build 2013-06-27 12:48:26 +0000
+++ ci-build 2013-06-27 12:48:26 +0000
@@ -1,6 +1,8 @@
1#!/bin/sh1#!/bin/sh
22
3VENV_DIR="/tmp/ci-build-venv"3VENV_DIR="/tmp/ci-build-venv"
4# Directory where coverage HTML report will be written.
5COVERAGE_REPORT_DIR="lava_tool_coverage"
46
5set -e7set -e
68
@@ -26,6 +28,10 @@
26if ! pip show mock | grep -q mock; then28if ! pip show mock | grep -q mock; then
27 pip install mock29 pip install mock
28fi30fi
31# Requirement to run code coverage tests.
32if ! pip show coverage | grep -q coverage; then
33 pip install coverage
34fi
2935
30export LAVACONFIG=/dev/null36export LAVACONFIG=/dev/null
3137
@@ -45,4 +51,10 @@
45 python -m unittest lava_tool.tests.test_suite < /dev/null51 python -m unittest lava_tool.tests.test_suite < /dev/null
46fi52fi
4753
54if test -d $COVERAGE_REPORT_DIR; then
55 rm -rf $COVERAGE_REPORT_DIR
56fi
57# Runs python-coverage.
58python-coverage run -m unittest lava_tool.tests.test_suite 2>/dev/null && python-coverage html
59
48./integration-tests60./integration-tests
4961
=== modified file 'lava/config.py'
--- lava/config.py 2013-06-27 12:48:26 +0000
+++ lava/config.py 2013-06-27 12:48:26 +0000
@@ -16,91 +16,192 @@
16# You should have received a copy of the GNU Lesser General Public License16# You should have received a copy of the GNU Lesser General Public License
17# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.17# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
1818
19"""
20Config class.
21"""
22
19import atexit23import atexit
20from ConfigParser import ConfigParser, NoOptionError, NoSectionError
21import os24import os
22import readline25import readline
2326
24__all__ = ['InteractiveConfig', 'NonInteractiveConfig']27from ConfigParser import (
2528 ConfigParser,
26history = os.path.join(os.path.expanduser("~"), ".lava_history")29 NoOptionError,
30 NoSectionError,
31)
32
33from lava.tool.errors import CommandError
34
35__all__ = ['Config', 'InteractiveConfig']
36
37# Store for function calls to be made at exit time.
38AT_EXIT_CALLS = set()
39# Config default section.
40DEFAULT_SECTION = "DEFAULT"
41
42HISTORY = os.path.join(os.path.expanduser("~"), ".lava_history")
27try:43try:
28 readline.read_history_file(history)44 readline.read_history_file(HISTORY)
29except IOError:45except IOError:
30 pass46 pass
31atexit.register(readline.write_history_file, history)47atexit.register(readline.write_history_file, HISTORY)
3248
33config_file = (os.environ.get('LAVACONFIG') or49
34 os.path.join(os.path.expanduser('~'), '.lavaconfig'))50def _run_at_exit():
35config_backend = ConfigParser()51 """Runs all the function at exit."""
36config_backend.read([config_file])52 for call in list(AT_EXIT_CALLS):
3753 call()
3854atexit.register(_run_at_exit)
39def save_config():55
40 with open(config_file, 'w') as f:56
41 config_backend.write(f)57class Config(object):
42atexit.register(save_config)58 """A generic config object."""
4359 def __init__(self):
4460 # The cache where to store parameters.
45class Parameter(object):
46
47 def __init__(self, id, depends=None):
48 self.id = id
49 self.depends = depends
50
51
52class InteractiveConfig(object):
53
54 def __init__(self, force_interactive=False):
55 self._force_interactive = force_interactive
56 self._cache = {}61 self._cache = {}
5762 self._config_file = (os.environ.get('LAVACONFIG') or
58 def get(self, parameter):63 os.path.join(os.path.expanduser('~'),
59 key = parameter.id64 '.lavaconfig'))
60 value = None65 self._config_backend = ConfigParser()
66 self._config_backend.read([self._config_file])
67 AT_EXIT_CALLS.add(self.save)
68
69 def _calculate_config_section(self, parameter):
70 """Calculates the config section of the specified parameter.
71
72 :param parameter: The parameter to calculate the section of.
73 :type Parameter
74 :return The config section.
75 """
76 section = DEFAULT_SECTION
61 if parameter.depends:77 if parameter.depends:
62 pass78 section = "{0}={1}".format(parameter.depends.id,
63 config_section = parameter.depends.id + '=' + self.get(parameter.depends)79 self.get(parameter.depends))
80 return section
81
82 def get(self, parameter, section=None):
83 """Retrieves a Parameter value.
84
85 The value is taken either from the Parameter itself, or from the cache,
86 or from the config file.
87
88 :param parameter: The parameter to search.
89 :type Parameter
90 :return The parameter value, or None if it is not found.
91 """
92 if not section:
93 section = self._calculate_config_section(parameter)
94 # Try to get the parameter value first if it has one.
95 if parameter.value:
96 value = parameter.value
64 else:97 else:
65 config_section = "DEFAULT"98 value = self._get_from_cache(parameter, section)
6699
67 if config_section in self._cache:100 if value is None:
68 if key in self._cache[config_section]:101 value = self._get_from_backend(parameter, section)
69 return self._cache[config_section][key]102 return value
70103
71 prompt = '%s: ' % key104 def _get_from_backend(self, parameter, section):
72105 """Gets the parameter value from the config backend.
106
107 :param parameter: The Parameter to look up.
108 :param section: The section in the Config.
109 """
110 value = None
73 try:111 try:
74 value = config_backend.get(config_section, key)112 value = self._config_backend.get(section, parameter.id)
75 except (NoOptionError, NoSectionError):113 except (NoOptionError, NoSectionError):
114 # Ignore, we return None.
76 pass115 pass
77 if value:116 return value
78 if self._force_interactive:117
79 prompt = "%s[%s]: " % (key, value)118 def _get_from_cache(self, parameter, section):
80 else:119 """Looks for the specified parameter in the internal cache.
81 return value120
82 try:121 :param parameter: The parameter to search.
83 user_input = raw_input(prompt).strip()122 :type Parameter
84 except EOFError:123 :return The parameter value, of None if it is not found.
85 user_input = None124 """
86 if user_input:125 value = None
87 value = user_input126 if section in self._cache.keys():
88 if not config_backend.has_section(config_section) and config_section != 'DEFAULT':127 if parameter.id in self._cache[section].keys():
89 config_backend.add_section(config_section)128 value = self._cache[section][parameter.id]
90 config_backend.set(config_section, key, value)129 return value
91130
92 if value:131 def _put_in_cache(self, key, value, section=DEFAULT_SECTION):
93 if config_section not in self._cache:132 """Insert the passed parameter in the internal cache.
94 self._cache[config_section] = {}133
95 self._cache[config_section][key] = value134 :param parameter: The parameter to insert.
96 return value135 :type Parameter
97 else:136 :param section: The name of the section in the config file.
98 raise KeyError(key)137 :type str
99138 """
100class NonInteractiveConfig(object):139 if section not in self._cache.keys():
101140 self._cache[section] = {}
102 def __init__(self, data):141 self._cache[section][key] = value
103 self.data = data142
104143 def put(self, key, value, section=DEFAULT_SECTION):
105 def get(self, parameter):144 """Adds a parameter to the config file.
106 return self.data[parameter.id]145
146 :param key: The key to add.
147 :param value: The value to add.
148 :param section: The name of the section as in the config file.
149 """
150 if (not self._config_backend.has_section(section) and
151 section != DEFAULT_SECTION):
152 self._config_backend.add_section(section)
153 self._config_backend.set(section, key, value)
154 # Store in the cache too.
155 self._put_in_cache(key, value, section)
156
157 def put_parameter(self, parameter, value=None, section=None):
158 """Adds a Parameter to the config file and cache.
159
160 :param Parameter: The parameter to add.
161 :param value: The value of the parameter. Defaults to None.
162 :param section: The section where this parameter should be stored.
163 Defaults to None.
164 """
165 if not section:
166 section = self._calculate_config_section(parameter)
167
168 if value is None and parameter.value is not None:
169 value = parameter.value
170 elif value is None:
171 raise CommandError("No value assigned to '{0}'.".format(
172 parameter.id))
173 self.put(parameter.id, value, section)
174
175 def save(self):
176 """Saves the config to file."""
177 with open(self._config_file, "w") as write_file:
178 self._config_backend.write(write_file)
179
180
181class InteractiveConfig(Config):
182 """An interactive config.
183
184 If a value is not found in the config file, it will ask it and then stores
185 it.
186 """
187 def __init__(self, force_interactive=True):
188 super(InteractiveConfig, self).__init__()
189 self._force_interactive = force_interactive
190
191 def get(self, parameter, section=None):
192 """Overrides the parent one.
193
194 The only difference with the parent one, is that it will ask to type
195 a parameter value in case it is not found.
196 """
197 if not section:
198 section = self._calculate_config_section(parameter)
199 value = super(InteractiveConfig, self).get(parameter, section)
200
201 if not (value is not None and parameter.asked):
202 if not value or self._force_interactive:
203 value = parameter.prompt(old_value=value)
204
205 if value is not None:
206 self.put(parameter.id, value, section)
207 return value
107208
=== modified file 'lava/device/__init__.py'
--- lava/device/__init__.py 2013-06-27 12:48:26 +0000
+++ lava/device/__init__.py 2013-06-27 12:48:26 +0000
@@ -18,11 +18,14 @@
1818
19import re19import re
2020
21from copy import deepcopy
22
21from lava.device.templates import (23from lava.device.templates import (
24 DEFAULT_TEMPLATE,
25 HOSTNAME_PARAMETER,
22 KNOWN_TEMPLATES,26 KNOWN_TEMPLATES,
23 DEFAULT_TEMPLATE,
24)27)
25from lava.tool.errors import CommandError28from lava.helper.template import expand_template
2629
2730
28def __re_compile(name):31def __re_compile(name):
@@ -44,60 +47,37 @@
4447
45class Device(object):48class Device(object):
46 """A generic device."""49 """A generic device."""
47 def __init__(self, hostname, template):50 def __init__(self, template, hostname=None):
48 self.device_type = None51 self.data = deepcopy(template)
49 self.hostname = hostname52 self.hostname = hostname
50 self.template = template.copy()
5153
52 def write(self, conf_file):54 def write(self, conf_file):
53 """Writes the object to file.55 """Writes the object to file.
5456
55 :param conf_file: The full path of the file where to write."""57 :param conf_file: The full path of the file where to write."""
56 with open(conf_file, 'w') as write_file:58 with open(conf_file, 'w') as write_file:
57 write_file.write(self.__str__())59 write_file.write(str(self))
5860
59 def _update(self):61 def update(self, config):
60 """Updates the template with the values specified for this class.62 """Updates the Device object values based on the provided config.
6163
62 Subclasses need to override this when they add more specific64 :param config: A Config instance.
63 attributes.
64 """65 """
65 # This is needed for the 'default' behavior. If we matched a known66 # We should always have a hostname, since it defaults to the name
66 # device, we do not need to update its device_type, since its already67 # given on the command line for the config file.
67 # defined in the template.68 if self.hostname is not None:
68 if self.device_type:69 # We do not ask the user again this parameter.
69 self.template.update(hostname=self.hostname,70 self.data[HOSTNAME_PARAMETER.id].asked = True
70 device_type=self.device_type)71 config.put(HOSTNAME_PARAMETER.id, self.hostname)
71 else:72
72 self.template.update(hostname=self.hostname)73 expand_template(self.data, config)
7374
74 def __str__(self):75 def __str__(self):
75 self._update()
76 string_list = []76 string_list = []
77 for key, value in self.template.iteritems():77 for key, value in self.data.iteritems():
78 if not value:
79 value = ''
80 string_list.append("{0} = {1}\n".format(str(key), str(value)))78 string_list.append("{0} = {1}\n".format(str(key), str(value)))
81 return "".join(string_list)79 return "".join(string_list)
8280
83 def __repr__(self):
84 self._update()
85 return str(self.template)
86
87
88def _get_device_type_from_user():
89 """Makes the user write what kind of device this is.
90
91 If something goes wrong, raises CommandError.
92 """
93 try:
94 dev_type = raw_input("Please specify the device type: ").strip()
95 except (EOFError, KeyboardInterrupt):
96 dev_type = None
97 if not dev_type:
98 raise CommandError("DEVICE name not specified or not correct.")
99 return dev_type
100
10181
102def get_known_device(name):82def get_known_device(name):
103 """Tries to match a device name with a known device type.83 """Tries to match a device name with a known device type.
@@ -105,20 +85,9 @@
105 :param name: The name of the device we want matched to a real device.85 :param name: The name of the device we want matched to a real device.
106 :return A Device instance.86 :return A Device instance.
107 """87 """
108 instance = None88 instance = Device(DEFAULT_TEMPLATE, name)
109 for known_dev, (matcher, dev_template) in KNOWN_DEVICES.iteritems():89 for known_dev, (matcher, dev_template) in KNOWN_DEVICES.iteritems():
110 if matcher.match(name):90 if matcher.match(name):
111 instance = Device(name, dev_template)91 instance = Device(dev_template, name)
112 if not instance:92 break
113 dev_type = _get_device_type_from_user()
114 known_dev = KNOWN_DEVICES.get(dev_type, None)
115 if known_dev:
116 instance = Device(name, known_dev[1])
117 else:
118 print ("Device '{0}' does not match a known "
119 "device.".format(dev_type))
120 instance = Device(name, DEFAULT_TEMPLATE)
121 # Not stricly necessary, users can fill up the field later.
122 instance.device_type = dev_type
123
124 return instance93 return instance
12594
=== modified file 'lava/device/commands.py'
--- lava/device/commands.py 2013-06-27 12:48:26 +0000
+++ lava/device/commands.py 2013-06-27 12:48:26 +0000
@@ -56,7 +56,7 @@
56 def invoke(self):56 def invoke(self):
57 real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX])57 real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX])
5858
59 if get_device_file(real_file_name):59 if get_device_file(real_file_name) is not None:
60 print >> sys.stdout, ("A device configuration file named '{0}' "60 print >> sys.stdout, ("A device configuration file named '{0}' "
61 "already exists.".format(real_file_name))61 "already exists.".format(real_file_name))
62 print >> sys.stdout, ("Use 'lava device config {0}' to edit "62 print >> sys.stdout, ("Use 'lava device config {0}' to edit "
@@ -68,6 +68,7 @@
68 real_file_name))68 real_file_name))
6969
70 device = get_known_device(self.args.DEVICE)70 device = get_known_device(self.args.DEVICE)
71 device.update(self.config)
71 device.write(device_conf_file)72 device.write(device_conf_file)
7273
73 print >> sys.stdout, ("Created device file '{0}' in: {1}".format(74 print >> sys.stdout, ("Created device file '{0}' in: {1}".format(
@@ -116,5 +117,4 @@
116 if device_conf and self.can_edit_file(device_conf):117 if device_conf and self.can_edit_file(device_conf):
117 self.edit_file(device_conf)118 self.edit_file(device_conf)
118 else:119 else:
119 raise CommandError("Cannot edit file '{0}' at: "120 raise CommandError("Cannot edit file '{0}'".format(real_file_name))
120 "{1}.".format(real_file_name, device_conf))
121121
=== modified file 'lava/device/templates.py'
--- lava/device/templates.py 2013-06-27 12:48:26 +0000
+++ lava/device/templates.py 2013-06-27 12:48:26 +0000
@@ -21,22 +21,50 @@
21will be used to serialize a Device object.21will be used to serialize a Device object.
22"""22"""
2323
24from copy import copy
25
26from lava.parameter import Parameter
27
28# The hostname parameter is always in the DEFAULT config section.
29HOSTNAME_PARAMETER = Parameter("hostname")
30DEVICE_TYPE_PARAMETER = Parameter("device_type", depends=HOSTNAME_PARAMETER)
31CONNECTION_COMMAND_PARMAETER = Parameter("connection_command",
32 depends=DEVICE_TYPE_PARAMETER)
33
24DEFAULT_TEMPLATE = {34DEFAULT_TEMPLATE = {
25 'device_type': None,35 'hostname': HOSTNAME_PARAMETER,
26 'hostname': None,36 'device_type': DEVICE_TYPE_PARAMETER,
27 'connection_command': None,37 'connection_command': CONNECTION_COMMAND_PARMAETER,
28}38}
2939
40# Specialized copies of the parameters.
41# We need this or we might end up asking the user twice the same parameter due
42# to different object references when one Parameter depends on a "specialized"
43# one, different from the defaults.
44PANDA_DEVICE_TYPE = copy(DEVICE_TYPE_PARAMETER)
45PANDA_DEVICE_TYPE.value = "panda"
46PANDA_DEVICE_TYPE.asked = True
47
48PANDA_CONNECTION_COMMAND = copy(CONNECTION_COMMAND_PARMAETER)
49PANDA_CONNECTION_COMMAND.depends = PANDA_DEVICE_TYPE
50
51VEXPRESS_DEVICE_TYPE = copy(DEVICE_TYPE_PARAMETER)
52VEXPRESS_DEVICE_TYPE.value = "vexpress"
53VEXPRESS_DEVICE_TYPE.asked = True
54
55VEXPRESS_CONNECTION_COMMAND = copy(CONNECTION_COMMAND_PARMAETER)
56VEXPRESS_CONNECTION_COMMAND.depends = VEXPRESS_DEVICE_TYPE
57
30# Dictionary with templates of known devices.58# Dictionary with templates of known devices.
31KNOWN_TEMPLATES = {59KNOWN_TEMPLATES = {
32 'panda': {60 'panda': {
33 'device_type': 'panda',61 'hostname': HOSTNAME_PARAMETER,
34 'hostname': None,62 'device_type': PANDA_DEVICE_TYPE,
35 'connection_command': None,63 'connection_command': PANDA_CONNECTION_COMMAND,
36 },64 },
37 'vexpress': {65 'vexpress': {
38 'device_type': 'vexpress',66 'hostname': HOSTNAME_PARAMETER,
39 'hostname': None,67 'device_type': VEXPRESS_DEVICE_TYPE,
40 'connection_command': None,68 'connection_command': VEXPRESS_CONNECTION_COMMAND,
41 },69 },
42}70}
4371
=== modified file 'lava/device/tests/test_commands.py'
--- lava/device/tests/test_commands.py 2013-06-27 12:48:26 +0000
+++ lava/device/tests/test_commands.py 2013-06-27 12:48:26 +0000
@@ -22,7 +22,11 @@
2222
23import os23import os
2424
25from mock import MagicMock, patch25from mock import (
26 MagicMock,
27 call,
28 patch,
29)
2630
27from lava.device.commands import (31from lava.device.commands import (
28 add,32 add,
@@ -33,12 +37,25 @@
33from lava.tool.errors import CommandError37from lava.tool.errors import CommandError
3438
3539
36class CommandsTest(HelperTest):40class AddCommandTest(HelperTest):
3741
42 def test_register_argument(self):
43 # Make sure that the parser add_argument is called and we have the
44 # correct argument.
45 add_command = add(self.parser, self.args)
46 add_command.register_arguments(self.parser)
47 name, args, kwargs = self.parser.method_calls[0]
48 self.assertIn("--non-interactive", args)
49
50 name, args, kwargs = self.parser.method_calls[1]
51 self.assertIn("DEVICE", args)
52
53 @patch("lava.device.Device.__str__", new=MagicMock(return_value=""))
54 @patch("lava.device.Device.update", new=MagicMock())
38 @patch("lava.device.commands.get_device_file",55 @patch("lava.device.commands.get_device_file",
39 new=MagicMock(return_value=None))56 new=MagicMock(return_value=None))
40 @patch("lava.device.commands.get_devices_path")57 @patch("lava.device.commands.get_devices_path")
41 def test_add_invoke(self, get_devices_path_mock):58 def test_add_invoke_0(self, get_devices_path_mock):
42 # Tests invocation of the add command. Verifies that the conf file is59 # Tests invocation of the add command. Verifies that the conf file is
43 # written to disk.60 # written to disk.
44 get_devices_path_mock.return_value = self.temp_dir61 get_devices_path_mock.return_value = self.temp_dir
@@ -51,6 +68,37 @@
51 ".".join([self.device, "conf"]))68 ".".join([self.device, "conf"]))
52 self.assertTrue(os.path.isfile(expected_path))69 self.assertTrue(os.path.isfile(expected_path))
5370
71 @patch("lava.device.commands.get_known_device")
72 @patch("lava.device.commands.get_devices_path")
73 @patch("lava.device.commands.sys.exit")
74 @patch("lava.device.commands.get_device_file")
75 def test_add_invoke_1(self, mocked_get_device_file, mocked_sys_exit,
76 mocked_get_devices_path, mocked_get_known_device):
77 mocked_get_devices_path.return_value = self.temp_dir
78 mocked_get_device_file.return_value = self.temp_file.name
79
80 add_command = add(self.parser, self.args)
81 add_command.edit_file = MagicMock()
82 add_command.invoke()
83
84 self.assertTrue(mocked_sys_exit.called)
85
86
87class RemoveCommandTests(HelperTest):
88
89 def test_register_argument(self):
90 # Make sure that the parser add_argument is called and we have the
91 # correct argument.
92 command = remove(self.parser, self.args)
93 command.register_arguments(self.parser)
94 name, args, kwargs = self.parser.method_calls[0]
95 self.assertIn("--non-interactive", args)
96
97 name, args, kwargs = self.parser.method_calls[1]
98 self.assertIn("DEVICE", args)
99
100 @patch("lava.device.Device.__str__", new=MagicMock(return_value=""))
101 @patch("lava.device.Device.update", new=MagicMock())
54 @patch("lava.device.commands.get_device_file")102 @patch("lava.device.commands.get_device_file")
55 @patch("lava.device.commands.get_devices_path")103 @patch("lava.device.commands.get_devices_path")
56 def test_remove_invoke(self, get_devices_path_mock, get_device_file_mock):104 def test_remove_invoke(self, get_devices_path_mock, get_device_file_mock):
@@ -83,6 +131,33 @@
83 remove_command = remove(self.parser, self.args)131 remove_command = remove(self.parser, self.args)
84 self.assertRaises(CommandError, remove_command.invoke)132 self.assertRaises(CommandError, remove_command.invoke)
85133
134
135class ConfigCommanTests(HelperTest):
136
137 def test_register_argument(self):
138 # Make sure that the parser add_argument is called and we have the
139 # correct argument.
140 command = config(self.parser, self.args)
141 command.register_arguments(self.parser)
142 name, args, kwargs = self.parser.method_calls[0]
143 self.assertIn("--non-interactive", args)
144
145 name, args, kwargs = self.parser.method_calls[1]
146 self.assertIn("DEVICE", args)
147
148 @patch("lava.device.commands.get_device_file")
149 def test_config_invoke_0(self, mocked_get_device_file):
150 command = config(self.parser, self.args)
151
152 mocked_get_device_file.return_value = self.temp_file.name
153 command.can_edit_file = MagicMock(return_value=True)
154 command.edit_file = MagicMock()
155 command.invoke()
156
157 self.assertTrue(command.edit_file.called)
158 self.assertEqual([call(self.temp_file.name)],
159 command.edit_file.call_args_list)
160
86 @patch("lava.device.commands.get_device_file",161 @patch("lava.device.commands.get_device_file",
87 new=MagicMock(return_value=None))162 new=MagicMock(return_value=None))
88 def test_config_invoke_raises_0(self):163 def test_config_invoke_raises_0(self):
89164
=== modified file 'lava/device/tests/test_device.py'
--- lava/device/tests/test_device.py 2013-06-27 12:48:26 +0000
+++ lava/device/tests/test_device.py 2013-06-27 12:48:26 +0000
@@ -20,10 +20,13 @@
20Device class unit tests.20Device class unit tests.
21"""21"""
2222
23import sys23from lava.parameter import Parameter
2424from lava.device.templates import (
25from StringIO import StringIO25 HOSTNAME_PARAMETER,
2626 PANDA_DEVICE_TYPE,
27 PANDA_CONNECTION_COMMAND,
28)
29from lava.tests.test_config import MockedConfig
27from lava.device import (30from lava.device import (
28 Device,31 Device,
29 get_known_device,32 get_known_device,
@@ -38,70 +41,73 @@
38 # User creates a new device with a guessable name for a device.41 # User creates a new device with a guessable name for a device.
39 instance = get_known_device('panda_new_01')42 instance = get_known_device('panda_new_01')
40 self.assertIsInstance(instance, Device)43 self.assertIsInstance(instance, Device)
41 self.assertEqual(instance.template['device_type'], 'panda')44 self.assertEqual(instance.data['device_type'].value, 'panda')
42 self.assertIsNone(instance.device_type)
4345
44 def test_get_known_device_panda_1(self):46 def test_get_known_device_panda_1(self):
45 # User creates a new device with a guessable name for a device.47 # User creates a new device with a guessable name for a device.
46 # Name passed has capital letters.48 # Name passed has capital letters.
47 instance = get_known_device('new_PanDa_02')49 instance = get_known_device('new_PanDa_02')
48 self.assertIsInstance(instance, Device)50 self.assertIsInstance(instance, Device)
49 self.assertEqual(instance.template['device_type'], 'panda')51 self.assertEqual(instance.data['device_type'].value, 'panda')
50 self.assertIsNone(instance.device_type)
5152
52 def test_get_known_device_vexpress_0(self):53 def test_get_known_device_vexpress_0(self):
53 # User creates a new device with a guessable name for a device.54 # User creates a new device with a guessable name for a device.
54 # Name passed has capital letters.55 # Name passed has capital letters.
55 instance = get_known_device('a_VexPress_Device')56 instance = get_known_device('a_VexPress_Device')
56 self.assertIsInstance(instance, Device)57 self.assertIsInstance(instance, Device)
57 self.assertEqual(instance.template['device_type'], 'vexpress')58 self.assertEqual(instance.data['device_type'].value, 'vexpress')
58 self.assertIsNone(instance.device_type)
5959
60 def test_get_known_device_vexpress_1(self):60 def test_get_known_device_vexpress_1(self):
61 # User creates a new device with a guessable name for a device.61 # User creates a new device with a guessable name for a device.
62 instance = get_known_device('another-vexpress')62 instance = get_known_device('another-vexpress')
63 self.assertIsInstance(instance, Device)63 self.assertIsInstance(instance, Device)
64 self.assertEqual(instance.template['device_type'], 'vexpress')64 self.assertIsInstance(instance.data['device_type'], Parameter)
65 self.assertIsNone(instance.device_type)65 self.assertEqual(instance.data['device_type'].value, 'vexpress')
6666
67 def test_instance_update(self):67 def test_device_update_1(self):
68 # Tests that when calling the _update() function with an known device68 # Tests that when calling update() on a Device, the template gets
69 # it does not update the device_type instance attribute, and that the69 # updated with the correct values from a Config instance.
70 # template contains the correct name.70 hostname = "panda_device"
71 instance = get_known_device('Another_PanDa_device')71
72 instance._update()72 config = MockedConfig(self.temp_file.name)
73 self.assertIsInstance(instance, Device)73 config.put_parameter(HOSTNAME_PARAMETER, hostname)
74 self.assertEqual(instance.template['device_type'], 'panda')74 config.put_parameter(PANDA_DEVICE_TYPE, "panda")
75 self.assertIsNone(instance.device_type)75 config.put_parameter(PANDA_CONNECTION_COMMAND, "test")
7676
77 def test_get_known_device_unknown(self):77 expected = {
78 # User tries to create a new device with an unknown device type. She78 "hostname": hostname,
79 # is asked to insert the device type and types 'a_fake_device'.79 "device_type": "panda",
80 sys.stdin = StringIO('a_fake_device')80 "connection_command": "test"
81 instance = get_known_device('a_fake_device')81 }
82 self.assertIsInstance(instance, Device)82
83 self.assertEqual(instance.device_type, 'a_fake_device')83 instance = get_known_device(hostname)
8484 instance.update(config)
85 def test_get_known_device_known(self):85
86 # User tries to create a new device with a not recognizable name.86 self.assertEqual(expected, instance.data)
87 # She is asked to insert the device type and types 'panda'.
88 sys.stdin = StringIO("panda")
89 instance = get_known_device("another_fake_device")
90 self.assertIsInstance(instance, Device)
91 self.assertEqual(instance.template["device_type"], "panda")
92
93 def test_get_known_device_raises(self):
94 # User tries to create a new device, but in some way nothing is passed
95 # on the command line when asked.
96 self.assertRaises(CommandError, get_known_device, 'a_fake_device')
9787
98 def test_device_write(self):88 def test_device_write(self):
99 # User tries to create a new panda device. The conf file is written89 # User tries to create a new panda device. The conf file is written
100 # and contains the expected results.90 # and contains the expected results.
101 expected = ("hostname = panda02\nconnection_command = \n"91 hostname = "panda_device"
92
93 config = MockedConfig(self.temp_file.name)
94 config.put_parameter(HOSTNAME_PARAMETER, hostname)
95 config.put_parameter(PANDA_DEVICE_TYPE, "panda")
96 config.put_parameter(PANDA_CONNECTION_COMMAND, "test")
97
98 expected = {
99 "hostname": hostname,
100 "device_type": "panda",
101 "connection_command": "test"
102 }
103
104 instance = get_known_device(hostname)
105 instance.update(config)
106 instance.write(self.temp_file.name)
107
108 expected = ("hostname = panda_device\nconnection_command = test\n"
102 "device_type = panda\n")109 "device_type = panda\n")
103 instance = get_known_device("panda02")110
104 instance.write(self.temp_file.name)
105 obtained = ""111 obtained = ""
106 with open(self.temp_file.name) as f:112 with open(self.temp_file.name) as f:
107 obtained = f.read()113 obtained = f.read()
108114
=== modified file 'lava/helper/command.py'
--- lava/helper/command.py 2013-06-27 12:48:26 +0000
+++ lava/helper/command.py 2013-06-27 12:48:26 +0000
@@ -93,7 +93,7 @@
93 :return The command execution return code.93 :return The command execution return code.
94 """94 """
95 if not isinstance(cmd_args, list):95 if not isinstance(cmd_args, list):
96 cmd_args = list(cmd_args)96 cmd_args = [cmd_args]
97 try:97 try:
98 return subprocess.check_call(cmd_args)98 return subprocess.check_call(cmd_args)
99 except subprocess.CalledProcessError:99 except subprocess.CalledProcessError:
100100
=== added file 'lava/helper/template.py'
--- lava/helper/template.py 1970-01-01 00:00:00 +0000
+++ lava/helper/template.py 2013-06-27 12:48:26 +0000
@@ -0,0 +1,44 @@
1# Copyright (C) 2013 Linaro Limited
2#
3# Author: Milo Casagrande <milo.casagrande@linaro.org>
4#
5# This file is part of lava-tool.
6#
7# lava-tool is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser General Public License version 3
9# as published by the Free Software Foundation
10#
11# lava-tool is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
18
19from lava.parameter import Parameter
20
21
22def expand_template(template, config):
23 """Updates a template based on the values from the provided config.
24
25 :param template: A template to be updated.
26 :param config: A Config instance where values should be taken.
27 """
28
29 def update(data):
30 """Internal recursive function."""
31 if isinstance(data, dict):
32 keys = data.keys()
33 elif isinstance(data, list):
34 keys = range(len(data))
35 else:
36 return
37 for key in keys:
38 entry = data[key]
39 if isinstance(entry, Parameter):
40 data[key] = config.get(entry)
41 else:
42 update(entry)
43
44 update(template)
045
=== modified file 'lava/helper/tests/test_command.py'
--- lava/helper/tests/test_command.py 2013-06-27 12:48:26 +0000
+++ lava/helper/tests/test_command.py 2013-06-27 12:48:26 +0000
@@ -18,6 +18,14 @@
1818
19"""lava.herlp.command module tests."""19"""lava.herlp.command module tests."""
2020
21import subprocess
22from mock import (
23 MagicMock,
24 call,
25 patch,
26)
27
28from lava.tool.errors import CommandError
21from lava.helper.command import BaseCommand29from lava.helper.command import BaseCommand
22from lava.helper.tests.helper_test import HelperTest30from lava.helper.tests.helper_test import HelperTest
2331
@@ -51,3 +59,65 @@
51 obtained = f.read()59 obtained = f.read()
5260
53 self.assertEqual(expected, obtained)61 self.assertEqual(expected, obtained)
62
63 @patch("lava.helper.command.subprocess")
64 def test_run_0(self, mocked_subprocess):
65 mocked_subprocess.check_call = MagicMock()
66 BaseCommand.run("foo")
67 self.assertEqual(mocked_subprocess.check_call.call_args_list,
68 [call(["foo"])])
69 self.assertTrue(mocked_subprocess.check_call.called)
70
71 @patch("lava.helper.command.subprocess.check_call")
72 def test_run_1(self, mocked_check_call):
73 mocked_check_call.side_effect = subprocess.CalledProcessError(1, "foo")
74 self.assertRaises(CommandError, BaseCommand.run, ["foo"])
75
76 @patch("lava.helper.command.subprocess")
77 @patch("lava.helper.command.has_command", return_value=False)
78 @patch("lava.helper.command.os.environ.get", return_value=None)
79 @patch("lava.helper.command.sys.exit")
80 def test_edit_file_0(self, mocked_sys_exit, mocked_env_get,
81 mocked_has_command, mocked_subprocess):
82 BaseCommand.edit_file(self.temp_file.name)
83 self.assertTrue(mocked_sys_exit.called)
84
85 @patch("lava.helper.command.subprocess")
86 @patch("lava.helper.command.has_command", side_effect=[True, False])
87 @patch("lava.helper.command.os.environ.get", return_value=None)
88 def test_edit_file_1(self, mocked_env_get, mocked_has_command,
89 mocked_subprocess):
90 mocked_subprocess.Popen = MagicMock()
91 BaseCommand.edit_file(self.temp_file.name)
92 expected = [call(["sensible-editor", self.temp_file.name])]
93 self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
94
95 @patch("lava.helper.command.subprocess")
96 @patch("lava.helper.command.has_command", side_effect=[False, True])
97 @patch("lava.helper.command.os.environ.get", return_value=None)
98 def test_edit_file_2(self, mocked_env_get, mocked_has_command,
99 mocked_subprocess):
100 mocked_subprocess.Popen = MagicMock()
101 BaseCommand.edit_file(self.temp_file.name)
102 expected = [call(["xdg-open", self.temp_file.name])]
103 self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
104
105 @patch("lava.helper.command.subprocess")
106 @patch("lava.helper.command.has_command", return_value=False)
107 @patch("lava.helper.command.os.environ.get", return_value="vim")
108 def test_edit_file_3(self, mocked_env_get, mocked_has_command,
109 mocked_subprocess):
110 mocked_subprocess.Popen = MagicMock()
111 BaseCommand.edit_file(self.temp_file.name)
112 expected = [call(["vim", self.temp_file.name])]
113 self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
114
115 @patch("lava.helper.command.subprocess")
116 @patch("lava.helper.command.has_command", return_value=False)
117 @patch("lava.helper.command.os.environ.get", return_value="vim")
118 def test_edit_file_4(self, mocked_env_get, mocked_has_command,
119 mocked_subprocess):
120 mocked_subprocess.Popen = MagicMock()
121 mocked_subprocess.Popen.side_effect = Exception()
122 self.assertRaises(CommandError, BaseCommand.edit_file,
123 self.temp_file.name)
54124
=== modified file 'lava/helper/tests/test_dispatcher.py'
--- lava/helper/tests/test_dispatcher.py 2013-06-27 12:48:26 +0000
+++ lava/helper/tests/test_dispatcher.py 2013-06-27 12:48:26 +0000
@@ -19,10 +19,12 @@
19"""lava.helper.dispatcher tests."""19"""lava.helper.dispatcher tests."""
2020
21import os21import os
22import tempfile
23
24from mock import patch
25
22from lava.tool.errors import CommandError26from lava.tool.errors import CommandError
23
24from lava.helper.tests.helper_test import HelperTest27from lava.helper.tests.helper_test import HelperTest
25
26from lava.helper.dispatcher import (28from lava.helper.dispatcher import (
27 choose_devices_path,29 choose_devices_path,
28)30)
@@ -30,6 +32,15 @@
3032
31class DispatcherTests(HelperTest):33class DispatcherTests(HelperTest):
3234
35 def setUp(self):
36 super(DispatcherTests, self).setUp()
37 self.devices_dir = os.path.join(tempfile.gettempdir(), "devices")
38 os.makedirs(self.devices_dir)
39
40 def tearDown(self):
41 super(DispatcherTests, self).tearDown()
42 os.removedirs(self.devices_dir)
43
33 def test_choose_devices_path_0(self):44 def test_choose_devices_path_0(self):
34 # Tests that when passing more than one path, the first writable one45 # Tests that when passing more than one path, the first writable one
35 # is returned.46 # is returned.
@@ -50,3 +61,17 @@
50 obtained = choose_devices_path([self.temp_dir])61 obtained = choose_devices_path([self.temp_dir])
51 self.assertEqual(expected_path, obtained)62 self.assertEqual(expected_path, obtained)
52 self.assertTrue(os.path.isdir(expected_path))63 self.assertTrue(os.path.isdir(expected_path))
64
65 def test_choose_devices_path_3(self):
66 # Tests that returns the already existing devices path.
67 obtained = choose_devices_path([tempfile.gettempdir()])
68 self.assertEqual(self.devices_dir, obtained)
69
70 @patch("__builtin__.open")
71 def test_choose_devices_path_4(self, mocked_open):
72 # Tests that when IOError is raised and we pass only one dir
73 # CommandError is raised.
74 mocked_open.side_effect = IOError()
75 self.assertRaises(CommandError, choose_devices_path,
76 [tempfile.gettempdir()])
77 self.assertTrue(mocked_open.called)
5378
=== modified file 'lava/job/__init__.py'
--- lava/job/__init__.py 2013-06-27 12:48:26 +0000
+++ lava/job/__init__.py 2013-06-27 12:48:26 +0000
@@ -19,7 +19,7 @@
19from copy import deepcopy19from copy import deepcopy
20import json20import json
2121
22from lava.job.templates import Parameter22from lava.helper.template import expand_template
2323
2424
25class Job:25class Job:
@@ -27,21 +27,7 @@
27 self.data = deepcopy(template)27 self.data = deepcopy(template)
2828
29 def fill_in(self, config):29 def fill_in(self, config):
3030 expand_template(self.data, config)
31 def insert_data(data):
32 if isinstance(data, dict):
33 keys = data.keys()
34 elif isinstance(data, list):
35 keys = range(len(data))
36 else:
37 return
38 for key in keys:
39 entry = data[key]
40 if isinstance(entry, Parameter):
41 data[key] = config.get(entry)
42 else:
43 insert_data(entry)
44 insert_data(self.data)
4531
46 def write(self, stream):32 def write(self, stream):
47 stream.write(json.dumps(self.data, indent=4))33 stream.write(json.dumps(self.data, indent=4))
4834
=== modified file 'lava/job/commands.py'
--- lava/job/commands.py 2013-06-27 12:48:26 +0000
+++ lava/job/commands.py 2013-06-27 12:48:26 +0000
@@ -25,12 +25,13 @@
25import xmlrpclib25import xmlrpclib
2626
27from lava.helper.command import BaseCommand27from lava.helper.command import BaseCommand
28from lava.helper.dispatcher import get_devices
2829
29from lava.config import Parameter
30from lava.job import Job30from lava.job import Job
31from lava.job.templates import (31from lava.job.templates import (
32 BOOT_TEST,32 BOOT_TEST,
33)33)
34from lava.parameter import Parameter
34from lava.tool.command import CommandGroup35from lava.tool.command import CommandGroup
35from lava.tool.errors import CommandError36from lava.tool.errors import CommandError
36from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend37from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend
@@ -126,7 +127,7 @@
126 def invoke(self):127 def invoke(self):
127 if os.path.isfile(self.args.FILE):128 if os.path.isfile(self.args.FILE):
128 if has_command("lava-dispatch"):129 if has_command("lava-dispatch"):
129 devices = self.get_devices()130 devices = get_devices()
130 if devices:131 if devices:
131 if len(devices) > 1:132 if len(devices) > 1:
132 device = self._choose_device(devices)133 device = self._choose_device(devices)
133134
=== modified file 'lava/job/templates.py'
--- lava/job/templates.py 2013-06-27 12:48:26 +0000
+++ lava/job/templates.py 2013-06-27 12:48:26 +0000
@@ -16,7 +16,7 @@
16# You should have received a copy of the GNU Lesser General Public License16# You should have received a copy of the GNU Lesser General Public License
17# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.17# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
1818
19from lava.config import Parameter19from lava.parameter import Parameter
2020
21device_type = Parameter("device_type")21device_type = Parameter("device_type")
22prebuilt_image = Parameter("prebuilt_image", depends=device_type)22prebuilt_image = Parameter("prebuilt_image", depends=device_type)
2323
=== modified file 'lava/job/tests/test_commands.py'
--- lava/job/tests/test_commands.py 2013-06-27 12:48:26 +0000
+++ lava/job/tests/test_commands.py 2013-06-27 12:48:26 +0000
@@ -25,15 +25,14 @@
2525
26from mock import MagicMock, patch26from mock import MagicMock, patch
2727
28from lava.config import NonInteractiveConfig, Parameter28from lava.config import Config
2929from lava.helper.tests.helper_test import HelperTest
30from lava.job.commands import (30from lava.job.commands import (
31 new,31 new,
32 run,32 run,
33 submit,33 submit,
34)34)
3535from lava.parameter import Parameter
36from lava.helper.tests.helper_test import HelperTest
37from lava.tool.errors import CommandError36from lava.tool.errors import CommandError
3837
3938
@@ -46,8 +45,9 @@
46 self.device_type = Parameter('device_type')45 self.device_type = Parameter('device_type')
47 self.prebuilt_image = Parameter('prebuilt_image',46 self.prebuilt_image = Parameter('prebuilt_image',
48 depends=self.device_type)47 depends=self.device_type)
49 self.config = NonInteractiveConfig(48 self.config = Config()
50 {'device_type': 'foo', 'prebuilt_image': 'bar'})49 self.config.put_parameter(self.device_type, 'foo')
50 self.config.put_parameter(self.prebuilt_image, 'bar')
5151
52 def tmp(self, filename):52 def tmp(self, filename):
53 """Returns a path to a non existent file.53 """Returns a path to a non existent file.
5454
=== modified file 'lava/job/tests/test_job.py'
--- lava/job/tests/test_job.py 2013-05-28 22:08:12 +0000
+++ lava/job/tests/test_job.py 2013-06-27 12:48:26 +0000
@@ -21,15 +21,29 @@
21"""21"""
2222
23import json23import json
24import os
25import tempfile
26
27from StringIO import StringIO
24from unittest import TestCase28from unittest import TestCase
25from StringIO import StringIO
2629
27from lava.config import NonInteractiveConfig30from lava.config import Config
28from lava.job.templates import *
29from lava.job import Job31from lava.job import Job
32from lava.job.templates import BOOT_TEST
33from lava.parameter import Parameter
34
3035
31class JobTest(TestCase):36class JobTest(TestCase):
3237
38 def setUp(self):
39 self.config_file = tempfile.NamedTemporaryFile(delete=False)
40 self.config = Config()
41 self.config._config_file = self.config_file.name
42
43 def tearDown(self):
44 if os.path.isfile(self.config_file.name):
45 os.unlink(self.config_file.name)
46
33 def test_from_template(self):47 def test_from_template(self):
34 template = {}48 template = {}
35 job = Job(template)49 job = Job(template)
@@ -37,21 +51,20 @@
37 self.assertIsNot(job.data, template)51 self.assertIsNot(job.data, template)
3852
39 def test_fill_in_data(self):53 def test_fill_in_data(self):
54 image = "/path/to/panda.img"
55 param1 = Parameter("device_type")
56 param2 = Parameter("prebuilt_image", depends=param1)
57 self.config.put_parameter(param1, "panda")
58 self.config.put_parameter(param2, image)
59
40 job = Job(BOOT_TEST)60 job = Job(BOOT_TEST)
41 image = "/path/to/panda.img"61 job.fill_in(self.config)
42 config = NonInteractiveConfig(
43 {
44 "device_type": "panda",
45 "prebuilt_image": image,
46 }
47 )
48 job.fill_in(config)
4962
50 self.assertEqual(job.data['device_type'], "panda")63 self.assertEqual(job.data['device_type'], "panda")
51 self.assertEqual(job.data['actions'][0]["parameters"]["image"], image)64 self.assertEqual(job.data['actions'][0]["parameters"]["image"], image)
5265
53 def test_write(self):66 def test_write(self):
54 orig_data = { "foo": "bar" }67 orig_data = {"foo": "bar"}
55 job = Job(orig_data)68 job = Job(orig_data)
56 output = StringIO()69 output = StringIO()
57 job.write(output)70 job.write(output)
@@ -60,7 +73,7 @@
60 self.assertEqual(data, orig_data)73 self.assertEqual(data, orig_data)
6174
62 def test_writes_nicely_formatted_json(self):75 def test_writes_nicely_formatted_json(self):
63 orig_data = { "foo": "bar" }76 orig_data = {"foo": "bar"}
64 job = Job(orig_data)77 job = Job(orig_data)
65 output = StringIO()78 output = StringIO()
66 job.write(output)79 job.write(output)
6780
=== added file 'lava/parameter.py'
--- lava/parameter.py 1970-01-01 00:00:00 +0000
+++ lava/parameter.py 2013-06-27 12:48:26 +0000
@@ -0,0 +1,75 @@
1# Copyright (C) 2013 Linaro Limited
2#
3# Author: Antonio Terceiro <antonio.terceiro@linaro.org>
4#
5# This file is part of lava-tool.
6#
7# lava-tool is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser General Public License version 3
9# as published by the Free Software Foundation
10#
11# lava-tool is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
18
19"""
20Parameter class and its accessory methods/functions.
21"""
22
23import sys
24
25
26class Parameter(object):
27 """A parameter with an optional dependency."""
28 def __init__(self, id, value=None, depends=None):
29 """Creates a new parameter.
30
31 :param id: The name of this parameter.
32 :param value: The value of this parameter. Defaults to None.
33 :param depends: If this Parameter depends on another one. Defaults
34 to None.
35 :type Parameter
36 """
37 self.id = id
38 self.value = value
39 self.depends = depends
40 self.asked = False
41
42 def prompt(self, old_value=None):
43 """Gets the parameter value from the user.
44
45 To get user input, the builtin `raw_input` function will be used. Input
46 will also be stripped of possible whitespace chars. If Enter or any
47 sort of whitespace chars in typed, the old Parameter value will be
48 returned.
49
50 :param old_value: The old parameter value.
51 :return The input as typed by the user, or the old value.
52 """
53 if old_value is not None:
54 prompt = "{0} [{1}]: ".format(self.id, old_value)
55 else:
56 prompt = "{0}: ".format(self.id)
57
58 user_input = None
59 try:
60 user_input = raw_input(prompt).strip()
61 except EOFError:
62 pass
63 except KeyboardInterrupt:
64 sys.exit(-1)
65
66 if user_input is not None:
67 if len(user_input) == 0 and old_value:
68 # Keep the old value when user press enter or another
69 # whitespace char.
70 self.value = old_value
71 else:
72 self.value = user_input
73
74 self.asked = True
75 return self.value
076
=== added directory 'lava/tests'
=== added file 'lava/tests/__init__.py'
=== added file 'lava/tests/test_config.py'
--- lava/tests/test_config.py 1970-01-01 00:00:00 +0000
+++ lava/tests/test_config.py 2013-06-27 12:48:26 +0000
@@ -0,0 +1,278 @@
1# Copyright (C) 2013 Linaro Limited
2#
3# Author: Milo Casagrande <milo.casagrande@linaro.org>
4#
5# This file is part of lava-tool.
6#
7# lava-tool is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser General Public License version 3
9# as published by the Free Software Foundation
10#
11# lava-tool is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
18
19"""
20lava.config unit tests.
21"""
22
23import os
24import sys
25import tempfile
26
27from StringIO import StringIO
28from mock import MagicMock, patch, call
29
30from lava.config import (
31 Config,
32 InteractiveConfig,
33 ConfigParser,
34)
35from lava.helper.tests.helper_test import HelperTest
36from lava.parameter import Parameter
37from lava.tool.errors import CommandError
38
39
40class MockedConfig(Config):
41 """A subclass of the original Config class.
42
43 Used to test the Config class, but to not have the same constructor in
44 order to use temporary files for the configuration.
45 """
46 def __init__(self, config_file):
47 self._cache = {}
48 self._config_file = config_file
49 self._config_backend = ConfigParser()
50 self._config_backend.read([self._config_file])
51
52
53class MockedInteractiveConfig(InteractiveConfig):
54 def __init__(self, config_file, force_interactive=False):
55 self._cache = {}
56 self._config_file = config_file
57 self._config_backend = ConfigParser()
58 self._config_backend.read([self._config_file])
59 self._force_interactive = force_interactive
60
61
62class ConfigTestCase(HelperTest):
63 """General test case class for the different Config classes."""
64 def setUp(self):
65 super(ConfigTestCase, self).setUp()
66 self.config_file = tempfile.NamedTemporaryFile(delete=False)
67
68 self.param1 = Parameter("foo")
69 self.param2 = Parameter("bar", depends=self.param1)
70
71 def tearDown(self):
72 super(ConfigTestCase, self).tearDown()
73 if os.path.isfile(self.config_file.name):
74 os.unlink(self.config_file.name)
75
76
77class ConfigTest(ConfigTestCase):
78
79 def setUp(self):
80 super(ConfigTest, self).setUp()
81 self.config = MockedConfig(self.config_file.name)
82
83 def test_assert_temp_config_file(self):
84 # Dummy test to make sure we are overriding correctly the Config class.
85 self.assertEqual(self.config._config_file, self.config_file.name)
86
87 def test_config_put_in_cache_0(self):
88 self.config._put_in_cache("key", "value", "section")
89 self.assertEqual(self.config._cache["section"]["key"], "value")
90
91 def test_config_get_from_cache_0(self):
92 self.config._put_in_cache("key", "value", "section")
93 obtained = self.config._get_from_cache(Parameter("key"), "section")
94 self.assertEqual("value", obtained)
95
96 def test_config_get_from_cache_1(self):
97 self.config._put_in_cache("key", "value", "DEFAULT")
98 obtained = self.config._get_from_cache(Parameter("key"), "DEFAULT")
99 self.assertEqual("value", obtained)
100
101 def test_config_put_0(self):
102 # Puts a value in the DEFAULT section.
103 self.config._put_in_cache = MagicMock()
104 self.config.put("foo", "foo")
105 expected = "foo"
106 obtained = self.config._config_backend.get("DEFAULT", "foo")
107 self.assertEqual(expected, obtained)
108
109 def test_config_put_1(self):
110 # Puts a value in a new section.
111 self.config._put_in_cache = MagicMock()
112 self.config.put("foo", "foo", "bar")
113 expected = "foo"
114 obtained = self.config._config_backend.get("bar", "foo")
115 self.assertEqual(expected, obtained)
116
117 def test_config_put_parameter_0(self):
118 self.config._calculate_config_section = MagicMock(return_value="")
119 self.assertRaises(CommandError, self.config.put_parameter, self.param1)
120
121 @patch("lava.config.Config.put")
122 def test_config_put_parameter_1(self, mocked_config_put):
123 self.config._calculate_config_section = MagicMock(
124 return_value="DEFAULT")
125
126 self.param1.value = "bar"
127 self.config.put_parameter(self.param1)
128
129 self.assertEqual(mocked_config_put.mock_calls,
130 [call("foo", "bar", "DEFAULT")])
131
132 def test_config_get_0(self):
133 # Tests that with a non existing parameter, it returns None.
134 param = Parameter("baz")
135 self.config._get_from_cache = MagicMock(return_value=None)
136 self.config._calculate_config_section = MagicMock(
137 return_value="DEFAULT")
138
139 expected = None
140 obtained = self.config.get(param)
141 self.assertEqual(expected, obtained)
142
143 def test_config_get_1(self):
144 self.config.put_parameter(self.param1, "foo")
145 self.config._get_from_cache = MagicMock(return_value=None)
146 self.config._calculate_config_section = MagicMock(
147 return_value="DEFAULT")
148
149 expected = "foo"
150 obtained = self.config.get(self.param1)
151 self.assertEqual(expected, obtained)
152
153 def test_calculate_config_section_0(self):
154 expected = "DEFAULT"
155 obtained = self.config._calculate_config_section(self.param1)
156 self.assertEqual(expected, obtained)
157
158 def test_calculate_config_section_1(self):
159 self.config.put_parameter(self.param1, "foo")
160 expected = "foo=foo"
161 obtained = self.config._calculate_config_section(self.param2)
162 self.assertEqual(expected, obtained)
163
164 def test_config_save(self):
165 self.config.put_parameter(self.param1, "foo")
166 self.config.save()
167
168 expected = "[DEFAULT]\nfoo = foo\n\n"
169 obtained = ""
170 with open(self.config_file.name) as tmp_file:
171 obtained = tmp_file.read()
172 self.assertEqual(expected, obtained)
173
174 @patch("lava.config.AT_EXIT_CALLS", spec=set)
175 def test_config_atexit_call_list(self, mocked_calls):
176 # Tests that the save() method is added to the set of atexit calls.
177 config = Config()
178 config._config_file = self.config_file.name
179 config.put_parameter(self.param1, "foo")
180
181 expected = [call.add(config.save)]
182
183 self.assertEqual(expected, mocked_calls.mock_calls)
184
185
186class InteractiveConfigTest(ConfigTestCase):
187
188 def setUp(self):
189 super(InteractiveConfigTest, self).setUp()
190 self.config = MockedInteractiveConfig(
191 config_file=self.config_file.name)
192
193 @patch("lava.config.Config.get", new=MagicMock(return_value=None))
194 def test_non_interactive_config_0(self):
195 # Mocked config default is not to be interactive.
196 # Try to get a value that does not exists, users just press enter when
197 # asked for a value. Value will be empty.
198 sys.stdin = StringIO("\n")
199 value = self.config.get(Parameter("foo"))
200 self.assertEqual("", value)
201
202 @patch("lava.config.Config.get", new=MagicMock(return_value="value"))
203 def test_non_interactive_config_1(self):
204 # Parent class config returns a value, but we are not interactive.
205 value = self.config.get(Parameter("foo"))
206 self.assertEqual("value", value)
207
208 @patch("lava.config.Config.get", new=MagicMock(return_value=None))
209 def test_non_interactive_config_2(self):
210 expected = "bar"
211 sys.stdin = StringIO(expected)
212 value = self.config.get(Parameter("foo"))
213 self.assertEqual(expected, value)
214
215 @patch("lava.config.Config.get", new=MagicMock(return_value="value"))
216 def test_interactive_config_0(self):
217 # We force to be interactive, meaning that even if a value is found,
218 # it will be asked anyway.
219 self.config._force_interactive = True
220 expected = "a_new_value"
221 sys.stdin = StringIO(expected)
222 value = self.config.get(Parameter("foo"))
223 self.assertEqual(expected, value)
224
225 @patch("lava.config.Config.get", new=MagicMock(return_value="value"))
226 def test_interactive_config_1(self):
227 # Force to be interactive, but when asked for the new value press
228 # Enter. The old value should be returned.
229 self.config._force_interactive = True
230 sys.stdin = StringIO("\n")
231 value = self.config.get(Parameter("foo"))
232 self.assertEqual("value", value)
233
234 def test_calculate_config_section_0(self):
235 self.config._force_interactive = True
236 obtained = self.config._calculate_config_section(self.param1)
237 expected = "DEFAULT"
238 self.assertEqual(expected, obtained)
239
240 def test_calculate_config_section_1(self):
241 self.param2.depends.asked = True
242 self.config._force_interactive = True
243 self.config.put(self.param1.id, "foo")
244 obtained = self.config._calculate_config_section(self.param2)
245 expected = "foo=foo"
246 self.assertEqual(expected, obtained)
247
248 def test_calculate_config_section_2(self):
249 self.config._force_interactive = True
250 self.config._config_backend.get = MagicMock(return_value=None)
251 sys.stdin = StringIO("baz")
252 expected = "foo=baz"
253 obtained = self.config._calculate_config_section(self.param2)
254 self.assertEqual(expected, obtained)
255
256 def test_calculate_config_section_3(self):
257 # Tests that when a parameter has its value in the cache and also on
258 # file, we honor the cached version.
259 self.param2.depends.asked = True
260 self.config._force_interactive = True
261 self.config._get_from_cache = MagicMock(return_value="bar")
262 self.config._config_backend.get = MagicMock(return_value="baz")
263 expected = "foo=bar"
264 obtained = self.config._calculate_config_section(self.param2)
265 self.assertEqual(expected, obtained)
266
267 @patch("lava.config.Config.get", new=MagicMock(return_value=None))
268 @patch("lava.parameter.sys.exit")
269 @patch("lava.parameter.raw_input", create=True)
270 def test_interactive_config_exit(self, mocked_raw, mocked_sys_exit):
271 self.config._calculate_config_section = MagicMock(
272 return_value="DEFAULT")
273
274 mocked_raw.side_effect = KeyboardInterrupt()
275
276 self.config._force_interactive = True
277 self.config.get(self.param1)
278 self.assertTrue(mocked_sys_exit.called)
0279
=== added file 'lava/tests/test_parameter.py'
--- lava/tests/test_parameter.py 1970-01-01 00:00:00 +0000
+++ lava/tests/test_parameter.py 2013-06-27 12:48:26 +0000
@@ -0,0 +1,51 @@
1# Copyright (C) 2013 Linaro Limited
2#
3# Author: Milo Casagrande <milo.casagrande@linaro.org>
4#
5# This file is part of lava-tool.
6#
7# lava-tool is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser General Public License version 3
9# as published by the Free Software Foundation
10#
11# lava-tool is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
18
19"""
20lava.parameter unit tests.
21"""
22
23import sys
24
25from StringIO import StringIO
26from mock import patch
27
28from lava.helper.tests.helper_test import HelperTest
29from lava.parameter import Parameter
30
31
32class ParameterTest(HelperTest):
33
34 def setUp(self):
35 super(ParameterTest, self).setUp()
36 self.parameter1 = Parameter("foo", value="baz")
37
38 def test_prompt_0(self):
39 # Tests that when we have a value in the parameters and the user press
40 # Enter, we get the old value back.
41 sys.stdin = StringIO("\n")
42 obtained = self.parameter1.prompt()
43 self.assertEqual(self.parameter1.value, obtained)
44
45 @patch("lava.parameter.raw_input", create=True)
46 def test_prompt_1(self, mocked_raw_input):
47 # Tests that with a value stored in the parameter, if and EOFError is
48 # raised when getting user input, we get back the old value.
49 mocked_raw_input.side_effect = EOFError()
50 obtained = self.parameter1.prompt()
51 self.assertEqual(self.parameter1.value, obtained)
052
=== modified file 'lava_tool/tests/__init__.py'
--- lava_tool/tests/__init__.py 2013-06-27 12:48:26 +0000
+++ lava_tool/tests/__init__.py 2013-06-27 12:48:26 +0000
@@ -44,6 +44,8 @@
44 'lava.job.tests.test_commands',44 'lava.job.tests.test_commands',
45 'lava.device.tests.test_device',45 'lava.device.tests.test_device',
46 'lava.device.tests.test_commands',46 'lava.device.tests.test_commands',
47 'lava.tests.test_config',
48 'lava.tests.test_parameter',
47 'lava.helper.tests.test_command',49 'lava.helper.tests.test_command',
48 'lava.helper.tests.test_dispatcher',50 'lava.helper.tests.test_dispatcher',
49 ]51 ]

Subscribers

People subscribed via source and target branches