Merge lp:~milo/lava-tool/device-parameters into lp:~linaro-validation/lava-tool/trunk
- device-parameters
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Antonio Terceiro | Needs Fixing | ||
Linaro Validation Team | Pending | ||
Review via email:
|
Commit message
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.
- 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.
- 231. By Milo Casagrande
-
Fixed typos.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Milo Casagrande (milo) wrote : | # |
Hello Antonio!
Thanks for going through this!
On Fri, Jun 21, 2013 at 1:40 PM, Antonio Terceiro
<email address hidden> wrote:
>
>> +class InteractiveConf
>> + """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_interacti
>> + super(Interacti
>> + self._force_
>> +
>> + def _calculate_
>> + """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
>> +
>> + cached_value = self._get_
>> + depend_section)
>> + config_value = self._get_
>> + depend_section)
>> +
>> + # Honor the cached value.
>> + value = cached_value or config_value
>> + if not value:
>> + value = self.get(
>> + parameter.
>> + section = "{0}={1}
>> + 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_
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.
>> +
>> + 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...
- 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
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 | ] |
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' ======= ====== tests.test_ suite 2>/dev/null && python-coverage html coverage` : open
> --- 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.
> +
> +* The report will be save in a directory called `lava_tool_
typo: save → saved
> +the `index.html` file in there to see the report. coverage` www.gnu. org/licenses/>. nfig', 'NonInteractive Config' ] join(os. path.expanduser ("~"), ".lava_history") fig'] join(os. path.expanduser ("~"), ".lava_history") read_history_ file(history) read_history_ file(HISTORY) register( readline. write_history_ file, history) get('LAVACONFIG ') or join(os. path.expanduser ('~'), '.lavaconfig')) backend. read([config_ file]) backend. write(f) register( save_config) ig(object) : ve=False) : interactive = force_interactive register( readline. write_history_ file, HISTORY) EXIT_CALLS) : register( _run_at_ exit)
> +
> +Notes:
> +
> + * To re-run the coverage report, you have to delete the `lava_tool_
> +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://
>
> +"""
> +Config class.
> +"""
> +
> import atexit
> -from ConfigParser import ConfigParser, NoOptionError, NoSectionError
> import os
> import readline
> -
> -__all__ = ['InteractiveCo
> -
> -history = os.path.
> +import sys
> +
> +
> +from ConfigParser import ConfigParser, NoOptionError, NoSectionError
> +
> +__all__ = ['Config', 'InteractiveCon
> +
> +# Store for function calls to be made at exit time.
> +AT_EXIT_CALLS = set()
> +# Config default section.
> +DEFAULT_SECTION = "DEFAULT"
> +
> +HISTORY = os.path.
> try:
> - readline.
> + readline.
> except IOError:
> pass
> -atexit.
> -
> -config_file = (os.environ.
> - os.path.
> -config_backend = ConfigParser()
> -config_
> -
> -
> -def save_config():
> - with open(config_file, 'w') as f:
> - config_
> -atexit.
> -
> -
> -class Parameter(object):
> -
> - def __init__(self, id, depends=None):
> - self.id = id
> - self.depends = depends
> -
> -
> -class InteractiveConf
> -
> - def __init__(self, force_interacti
> - self._force_
> +atexit.
> +
> +
> +def _run_at_exit():
> + """Runs all the function at exit."""
> + for call in list(AT_
> + call()
> +atexit.
> +
> +
> +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
> - ...