Merge lp:~zyga/lava-test/support-lava-command into lp:lava-test/0.0
- support-lava-command
- Merge into trunk
Proposed by
Zygmunt Krynicki
Status: | Rejected |
---|---|
Rejected by: | Neil Williams |
Proposed branch: | lp:~zyga/lava-test/support-lava-command |
Merge into: | lp:lava-test/0.0 |
Diff against target: |
529 lines (+478/-3) 5 files modified
doc/changes.rst (+9/-0) lava/__init__.py (+3/-0) lava/test/commands.py (+453/-0) lava_test/commands.py (+1/-3) setup.py (+12/-0) |
To merge this branch: | bzr merge lp:~zyga/lava-test/support-lava-command |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Linaro Validation Team | Pending | ||
Review via email: mp+100471@code.launchpad.net |
Commit message
Description of the change
This branch adds support for lava top-level command by duplicating all of the commands there. Some commands are altered to make the user interface better.
The old lava-test implementation is left untouched.
To post a comment you must log in.
Revision history for this message
Marcin Juszkiewicz (hrw) wrote : | # |
Revision history for this message
Zygmunt Krynicki (zyga) wrote : | # |
ping
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote : | # |
This seems OK, but I assume it's quite a duplication of code?
Unmerged revisions
- 142. By Zygmunt Krynicki
-
Make lava test parse display what it is looking for
- 141. By Zygmunt Krynicki
-
Don't import and print version of linaro-json
- 140. By Zygmunt Krynicki
-
Create hierarchical version of all commands
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'doc/changes.rst' |
2 | --- doc/changes.rst 2012-03-27 12:42:34 +0000 |
3 | +++ doc/changes.rst 2012-04-02 16:50:25 +0000 |
4 | @@ -6,6 +6,15 @@ |
5 | Version 0.7 (UNRELEASED) |
6 | ======================== |
7 | |
8 | +* Add a hierachical version of all commands. Now ``lava-test`` can be invoked as ``lava test``. |
9 | +* Rename ``lava-test register-test`` to ``lava test register`` and ``lava-test |
10 | + unregister-test`` to ``lava test unregister``. |
11 | +* Rename ``lava-test list-tests`` to ``lava test list``. |
12 | +* Merge ``lava test list-installed`` with ``lava test list``. The optional |
13 | + --installed argument will now do what was previously a separate command. |
14 | +* Change how ``lava test reset`` displays progress |
15 | +* Make ``lava test parse`` display the files it is looking for |
16 | + |
17 | .. _version_0_6: |
18 | |
19 | Version 0.6 |
20 | |
21 | === added directory 'lava' |
22 | === added file 'lava/__init__.py' |
23 | --- lava/__init__.py 1970-01-01 00:00:00 +0000 |
24 | +++ lava/__init__.py 2012-04-02 16:50:25 +0000 |
25 | @@ -0,0 +1,3 @@ |
26 | +__import__('pkg_resources').declare_namespace(__name__) |
27 | +# DO NOT ADD ANYTHING TO THIS FILE! |
28 | +# IT MUST STAY AS IS (empty apart from the two lines above) |
29 | |
30 | === added directory 'lava/test' |
31 | === added file 'lava/test/__init__.py' |
32 | === added file 'lava/test/commands.py' |
33 | --- lava/test/commands.py 1970-01-01 00:00:00 +0000 |
34 | +++ lava/test/commands.py 2012-04-02 16:50:25 +0000 |
35 | @@ -0,0 +1,453 @@ |
36 | +""" |
37 | +lava.test.commands |
38 | +==================== |
39 | + |
40 | +LAVA command definitions used by lava-test |
41 | +""" |
42 | + |
43 | +import logging |
44 | +import os |
45 | +import shutil |
46 | +import subprocess |
47 | + |
48 | +from lava.tool.command import Command as LavaCommand, CommandGroup |
49 | +from lava.tool.errors import CommandError |
50 | + |
51 | +from lava_test.api.observers import ( |
52 | + ITestInstallerObserver, |
53 | + ITestRunnerObserver) |
54 | +from lava_test.core.artifacts import TestArtifacts |
55 | +from lava_test.core.config import get_config |
56 | +from lava_test.core.loader import TestLoader |
57 | +from lava_test.utils import Cache |
58 | + |
59 | + |
60 | +__all__ = ["test", "list_", "install", "uninstall", "run", "parse", "show", |
61 | + "register", "unregister", "reset"] |
62 | + |
63 | + |
64 | +class test(CommandGroup): |
65 | + """ |
66 | + Access test commands. |
67 | |
68 | + LAVA Test is a generic test wrapper framework used by the LAVA stack to run |
69 | + third party tests. |
70 | + """ |
71 | + |
72 | + namespace = "lava.test.commands" |
73 | + |
74 | + |
75 | +class ObserverMethods(ITestInstallerObserver, ITestRunnerObserver): |
76 | + """ |
77 | + Helper class that implements observer methods |
78 | + """ |
79 | + |
80 | + def about_to_install_packages(self, package_list): |
81 | + if self.args.quiet: |
82 | + return |
83 | + self.say("Installing packages: {0}", ", ".join(package_list)) |
84 | + |
85 | + def about_to_run_shell_command(self, cmd): |
86 | + if self.args.quiet: |
87 | + return |
88 | + self.say("Running shell command: {0!r}", cmd) |
89 | + |
90 | + def about_to_download_file(self, url): |
91 | + if self.args.quiet: |
92 | + return |
93 | + self.say("Downloading file from: {0!r}", url) |
94 | + |
95 | + def did_install_packages(self, package_list): |
96 | + pass |
97 | + |
98 | + def did_run_shell_command(self, cmd, returncode): |
99 | + if returncode is None: |
100 | + self.say("Command {0!r} was terminated prematurely", cmd) |
101 | + elif returncode != 0: |
102 | + self.say("Command {0!r} returned non-zero exit status {1}", |
103 | + cmd, returncode) |
104 | + |
105 | + def did_download_file(self, url): |
106 | + pass |
107 | + |
108 | + def display_subprocess_output(self, stream_name, line): |
109 | + if self.args.quiet_subcommands: |
110 | + return |
111 | + if stream_name == 'stdout': |
112 | + self.say('(stdout) {0}', line.rstrip()) |
113 | + elif stream_name == 'stderr': |
114 | + self.say('(stderr) {0}', line.rstrip()) |
115 | + |
116 | + |
117 | +class Command(LavaCommand, ObserverMethods): |
118 | + |
119 | + def __init__(self, parser, args): |
120 | + super(Command, self).__init__(parser, args) |
121 | + self._config = get_config() |
122 | + self._test_loader = TestLoader(self._config) |
123 | + # XXX: check how this interacts with top-level lava logger |
124 | + if self.args.verbose: |
125 | + logging.root.setLevel(logging.DEBUG) |
126 | + |
127 | + @classmethod |
128 | + def register_arguments(cls, parser): |
129 | + parser.add_argument( |
130 | + "-v", "--verbose", |
131 | + action="store_true", |
132 | + default=False, |
133 | + help="Be verbose about undertaken actions") |
134 | + parser.add_argument( |
135 | + "-q", "--quiet", |
136 | + action="store_true", |
137 | + default=False, |
138 | + help="Be less verbose about undertaken actions") |
139 | + parser.add_argument( |
140 | + "-Q", "--quiet-subcommands", |
141 | + action="store_true", |
142 | + default=False, |
143 | + help="Hide the output of all sub-commands (including tests)") |
144 | + |
145 | + |
146 | +class TestAffectingCommand(Command): |
147 | + |
148 | + INSTALL_REQUIRED = False |
149 | + |
150 | + @classmethod |
151 | + def register_arguments(cls, parser): |
152 | + super(TestAffectingCommand, cls).register_arguments(parser) |
153 | + parser.add_argument( |
154 | + "test_id", help="Test or test suite identifier") |
155 | + |
156 | + def invoke(self): |
157 | + try: |
158 | + test = self._test_loader[self.args.test_id] |
159 | + except KeyError: |
160 | + try: |
161 | + test = self._test_loader[self.args.test_id.replace('-', '_')] |
162 | + except KeyError: |
163 | + raise CommandError( |
164 | + "There is no test with the specified ID") |
165 | + return self.invoke_with_test(test) |
166 | + |
167 | + |
168 | +class list_(Command): |
169 | + """ |
170 | + List available tests |
171 | + |
172 | + .. program:: lava-test list-tests |
173 | + |
174 | + Lists all available tests, grouping them by provider. |
175 | + """ |
176 | + |
177 | + @classmethod |
178 | + def get_name(cls): |
179 | + return "list" |
180 | + |
181 | + @classmethod |
182 | + def register_arguments(cls, parser): |
183 | + super(list_, cls).register_arguments(parser) |
184 | + parser.add_argument( |
185 | + "--installed", |
186 | + action="store_true", |
187 | + default=False, |
188 | + help="Display only installed tests") |
189 | + |
190 | + def invoke(self): |
191 | + for provider in self._test_loader.get_providers(): |
192 | + test_list = [provider[test_id] for test_id in provider] |
193 | + if not test_list: |
194 | + continue |
195 | + self.say("{0}", provider.description) |
196 | + for test in test_list: |
197 | + if self.args.installed and not test.is_installed: |
198 | + continue |
199 | + self.say(" - {test_id}", test_id=test.test_id) |
200 | + |
201 | + |
202 | +class install(TestAffectingCommand): |
203 | + """ |
204 | + Install a test program |
205 | + """ |
206 | + |
207 | + def invoke_with_test(self, test): |
208 | + if test.is_installed: |
209 | + raise CommandError("This test is already installed") |
210 | + try: |
211 | + test.install(self) |
212 | + except (subprocess.CalledProcessError, RuntimeError) as ex: |
213 | + raise CommandError(str(ex)) |
214 | + |
215 | + |
216 | +class uninstall(TestAffectingCommand): |
217 | + """ |
218 | + Uninstall a test program |
219 | + """ |
220 | + |
221 | + def invoke_with_test(self, test): |
222 | + if not test.is_installed: |
223 | + raise CommandError("This test is not installed") |
224 | + test.uninstall() |
225 | + |
226 | + |
227 | +class run(TestAffectingCommand): |
228 | + """ |
229 | + Run a previously installed test program |
230 | + """ |
231 | + |
232 | + @classmethod |
233 | + def register_arguments(cls, parser): |
234 | + super(run, cls).register_arguments(parser) |
235 | + group = parser.add_argument_group("initial bundle configuration") |
236 | + group.add_argument( |
237 | + "-S", "--skip-software-context", |
238 | + default=False, |
239 | + action="store_true", |
240 | + help=("Do not store the software context in the" |
241 | + " initial bundle. Typically this saves OS" |
242 | + " image name and all the installed software" |
243 | + " packages.")) |
244 | + group.add_argument( |
245 | + "-H", "--skip-hardware-context", |
246 | + default=False, |
247 | + action="store_true", |
248 | + help=("Do not store the hardware context in the" |
249 | + " initial bundle. Typically this saves CPU," |
250 | + " memory and USB device information.")) |
251 | + group.add_argument( |
252 | + "--trusted-time", |
253 | + default=False, |
254 | + action="store_true", |
255 | + help=("Indicate that the real time clock has" |
256 | + " accurate data. This can differentiate" |
257 | + " test results created on embedded devices" |
258 | + " that often have inaccurate real time" |
259 | + " clock settings.")) |
260 | + group.add_argument( |
261 | + "--analyzer-assigned-uuid", |
262 | + default=None, |
263 | + metavar="UUID", |
264 | + help=("Set the analyzer_assigned_uuid to the specified value." |
265 | + " This will prevent the test device from attempting" |
266 | + " to generate an UUID by itself. This option may be" |
267 | + " required if the test device has unreliable real" |
268 | + " time clock (no battery backed, not ensure to be" |
269 | + " up-to-date) and unreliable/random hardware ethernet " |
270 | + " address.")) |
271 | + |
272 | + group = parser.add_argument_group("complete bundle configuration") |
273 | + group.add_argument( |
274 | + "-o", "--output", |
275 | + default=None, |
276 | + metavar="FILE", |
277 | + help=("After running the test parse the result" |
278 | + " artifacts, fuse them with the initial" |
279 | + " bundle and finally save the complete bundle" |
280 | + " to the specified FILE.")) |
281 | + group.add_argument( |
282 | + "-A", "--skip-attachments", |
283 | + default=False, |
284 | + action="store_true", |
285 | + help=("Do not store standard output and standard" |
286 | + " error log files as attachments. This" |
287 | + " option is only affecting the bundle" |
288 | + " created with --output, the initial bundle" |
289 | + " is not affected as it never stores any" |
290 | + " attachments.")) |
291 | + |
292 | + parser.add_argument( |
293 | + "-t", "--test-options", |
294 | + default=None, |
295 | + help=( |
296 | + "Override the default test options." |
297 | + " The value is passed verbatim to test definition. Typically" |
298 | + " this is simply used in shell commands by expanding the" |
299 | + " string $(OPTIONS). Please refer to the built-in" |
300 | + " peacekeeper.py for examples. Depending on your shell you" |
301 | + " probably have to escape spaces and other special" |
302 | + " characters if you wish to include them in your argument" |
303 | + " options.")) |
304 | + |
305 | + def invoke_with_test(self, test): |
306 | + # Validate analyzer_assigned_uuid |
307 | + if self.args.analyzer_assigned_uuid: |
308 | + import uuid |
309 | + try: |
310 | + self.analyzer_assigned_uuid = str( |
311 | + uuid.UUID(self.args.analyzer_assigned_uuid)) |
312 | + except ValueError as exc: |
313 | + self.parser.error("--analyzer-assigned-uuid: %s" % exc) |
314 | + if not test.is_installed: |
315 | + raise CommandError("The specified test is not installed") |
316 | + try: |
317 | + artifacts, run_fail = test.run( |
318 | + self, test_options=self.args.test_options) |
319 | + except subprocess.CalledProcessError as ex: |
320 | + if ex.returncode is None: |
321 | + raise CommandError("Command %r was aborted" % ex.cmd) |
322 | + else: |
323 | + raise CommandError(str(ex)) |
324 | + except RuntimeError as ex: |
325 | + raise CommandError(str(ex)) |
326 | + self.say("run complete, result_id is {0!r}", artifacts.result_id) |
327 | + try: |
328 | + artifacts.create_initial_bundle( |
329 | + self.args.skip_software_context, |
330 | + self.args.skip_hardware_context, |
331 | + self.args.trusted_time, |
332 | + self.args.analyzer_assigned_uuid) |
333 | + except ImportError as exc: |
334 | + msg_template = ( |
335 | + "Unable to probe for software context. Install the '%s'" |
336 | + " package or invoke lava-test run with" |
337 | + " '--skip-software-context'") |
338 | + if exc.message == "No module named apt": |
339 | + raise CommandError(msg_template % "python-apt") |
340 | + elif exc.message == "No module named lsb_release": |
341 | + raise CommandError(msg_template % "lsb-release") |
342 | + else: |
343 | + raise |
344 | + artifacts.save_bundle() |
345 | + if self.args.output: |
346 | + parse_results = test.parse(artifacts) |
347 | + artifacts.incorporate_parse_results(parse_results) |
348 | + if not self.args.skip_attachments: |
349 | + artifacts.attach_standard_files_to_bundle() |
350 | + artifacts.save_bundle_as(self.args.output) |
351 | + if run_fail: |
352 | + raise CommandError( |
353 | + 'Some of test steps returned non-zero exit code') |
354 | + |
355 | + |
356 | +class parse(TestAffectingCommand): |
357 | + """ |
358 | + Parse the results of previous test run |
359 | + """ |
360 | + |
361 | + @classmethod |
362 | + def register_arguments(cls, parser): |
363 | + super(parse, cls).register_arguments(parser) |
364 | + parser.add_argument( |
365 | + "result_id", |
366 | + help="Test run result identifier") |
367 | + group = parser.add_argument_group("complete bundle configuration") |
368 | + group.add_argument( |
369 | + "-o", "--output", |
370 | + default=None, |
371 | + metavar="FILE", |
372 | + help=("After running the test parse the result" |
373 | + " artifacts, fuse them with the initial" |
374 | + " bundle and finally save the complete bundle" |
375 | + " to the specified FILE.")) |
376 | + group.add_argument( |
377 | + "-A", "--skip-attachments", |
378 | + default=False, |
379 | + action="store_true", |
380 | + help=("Do not store standard output and standard" |
381 | + " error log files as attachments. This" |
382 | + " option is only affecting the bundle" |
383 | + " created with --output, the initial bundle" |
384 | + " is not affected as it never stores any" |
385 | + " attachments.")) |
386 | + |
387 | + def invoke_with_test(self, test): |
388 | + artifacts = TestArtifacts( |
389 | + self.args.test_id, self.args.result_id, self._config) |
390 | + self.say("Loading test data from {0}", artifacts.bundle_pathname) |
391 | + if not os.path.exists(artifacts.bundle_pathname): |
392 | + raise CommandError("Specified result does not exist") |
393 | + artifacts.load_bundle() |
394 | + parse_results = test.parse(artifacts) |
395 | + artifacts.incorporate_parse_results(parse_results) |
396 | + self.say("Parsed {0} test results", |
397 | + len(artifacts.bundle["test_runs"][0]["test_results"])) |
398 | + logging.debug(artifacts.dumps_bundle()) |
399 | + if self.args.output: |
400 | + if not self.args.skip_attachments: |
401 | + artifacts.attach_standard_files_to_bundle() |
402 | + artifacts.save_bundle_as(self.args.output) |
403 | + |
404 | + |
405 | +class show(Command): |
406 | + """ |
407 | + Display the output from a previous test run |
408 | + """ |
409 | + |
410 | + @classmethod |
411 | + def register_arguments(cls, parser): |
412 | + super(show, cls).register_arguments(parser) |
413 | + parser.add_argument( |
414 | + "result_id", help="Test run result identifier") |
415 | + |
416 | + def invoke(self): |
417 | + artifacts = TestArtifacts(None, self.args.result_id, self._config) |
418 | + if not os.path.exists(artifacts.results_dir): |
419 | + raise CommandError("Specified result does not exist") |
420 | + if os.path.exists(artifacts.stdout_pathname): |
421 | + with open(artifacts.stdout_pathname, "rt") as stream: |
422 | + for line in iter(stream.readline, ''): |
423 | + self.display_subprocess_output("stdout", line) |
424 | + if os.path.exists(artifacts.stderr_pathname): |
425 | + with open(artifacts.stderr_pathname, "rt") as stream: |
426 | + for line in iter(stream.readline, ''): |
427 | + self.display_subprocess_output("stderr", line) |
428 | + |
429 | + |
430 | +class register(Command): |
431 | + """ |
432 | + Register remote test |
433 | + """ |
434 | + |
435 | + @classmethod |
436 | + def register_arguments(cls, parser): |
437 | + super(register, cls).register_arguments(parser) |
438 | + parser.add_argument( |
439 | + "test_url", help="Url for test definition file") |
440 | + |
441 | + def invoke(self): |
442 | + try: |
443 | + from lava_test.core.providers import RegistryProvider |
444 | + RegistryProvider.register_remote_test(self.args.test_url) |
445 | + except ValueError as exc: |
446 | + raise CommandError("Unable to register test: %s" % exc) |
447 | + except KeyError: |
448 | + raise CommandError("There is no test_url") |
449 | + |
450 | + |
451 | +class unregister(Command): |
452 | + """ |
453 | + Remove a declarative test from the registry |
454 | |
455 | + This command does the reverse of lava-test register. You need to pass the |
456 | + same URL you've used in `lava-test register-test` |
457 | + """ |
458 | + |
459 | + @classmethod |
460 | + def register_arguments(cls, parser): |
461 | + super(unregister, cls).register_arguments(parser) |
462 | + parser.add_argument( |
463 | + "url", |
464 | + metavar="URL", |
465 | + help="URL of the test definition file") |
466 | + |
467 | + def invoke(self): |
468 | + from lava_test.core.providers import RegistryProvider |
469 | + try: |
470 | + RegistryProvider.unregister_remote_test(self.args.url) |
471 | + except ValueError: |
472 | + raise CommandError("This test is not registered") |
473 | + |
474 | + |
475 | +class reset(Command): |
476 | + """ |
477 | + Reset the lava-test environment by removing cached items, all registered |
478 | + simple declarative tests and configuration files |
479 | + """ |
480 | + |
481 | + def invoke(self): |
482 | + self.say("Deleting {0}", self._config.configdir) |
483 | + shutil.rmtree(self._config.configdir, ignore_errors=True) |
484 | + self.say("Deleting {0}", self._config.installdir) |
485 | + shutil.rmtree(self._config.installdir, ignore_errors=True) |
486 | + self.say("Deleting {0}", self._config.resultsdir) |
487 | + shutil.rmtree(self._config.resultsdir, ignore_errors=True) |
488 | + cache = Cache.get_instance() |
489 | + self.say("Deleting {0}", cache.cache_dir) |
490 | + shutil.rmtree(cache.cache_dir, ignore_errors=True) |
491 | |
492 | === modified file 'lava_test/commands.py' |
493 | --- lava_test/commands.py 2012-03-19 19:24:05 +0000 |
494 | +++ lava_test/commands.py 2012-04-02 16:50:25 +0000 |
495 | @@ -400,12 +400,10 @@ |
496 | import lava_tool |
497 | import lava_test |
498 | import linaro_dashboard_bundle |
499 | - import linaro_json |
500 | return [ |
501 | lava_test, |
502 | lava_tool, |
503 | - linaro_dashboard_bundle, |
504 | - linaro_json] |
505 | + linaro_dashboard_bundle] |
506 | |
507 | |
508 | class register_test(Command): |
509 | |
510 | === modified file 'setup.py' |
511 | --- setup.py 2012-03-02 14:21:13 +0000 |
512 | +++ setup.py 2012-04-02 16:50:25 +0000 |
513 | @@ -32,6 +32,18 @@ |
514 | entry_points=""" |
515 | [console_scripts] |
516 | lava-test=lava_test.main:main |
517 | + [lava.commands] |
518 | + test=lava.test.commands:test |
519 | + [lava.test.commands] |
520 | + list=lava.test.commands:list_ |
521 | + install=lava.test.commands:install |
522 | + uninstall=lava.test.commands:uninstall |
523 | + run=lava.test.commands:run |
524 | + parse=lava.test.commands:parse |
525 | + show=lava.test.commands:show |
526 | + register=lava.test.commands:register |
527 | + unregister=lava.test.commands:unregister |
528 | + reset=lava.test.commands:reset |
529 | [lava_test.commands] |
530 | version=lava_test.commands:version |
531 | list-tests=lava_test.commands:list_tests |
Looks nice. I have linaro-json part already applied in my packages.