Merge ~sylvain-pineau/checkbox-ng:drop-legacy-commands into checkbox-ng:master
- Git
- lp:~sylvain-pineau/checkbox-ng
- drop-legacy-commands
- Merge into master
Status: | Merged |
---|---|
Approved by: | Taihsiang Ho |
Approved revision: | 61decd188b368123be99f6998b09e9ab9b2810d0 |
Merged at revision: | 6442bbd13f4d4c3572913fd002b9c9a0bad7bb62 |
Proposed branch: | ~sylvain-pineau/checkbox-ng:drop-legacy-commands |
Merge into: | checkbox-ng:master |
Diff against target: |
2863 lines (+83/-428) 6 files modified
checkbox_ng/config.py (+0/-41) checkbox_ng/launcher/checkbox_cli.py (+5/-4) checkbox_ng/launcher/subcommands.py (+71/-4) dev/null (+0/-363) po/POTFILES.in (+7/-13) setup.py (+0/-3) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Taihsiang Ho | Approve | ||
Maciej Kisielewski (community) | Approve | ||
Review via email: mp+325213@code.launchpad.net |
Commit message
Description of the change
Drop legacy commands (including the checkbox sru) to only keep checkbox-cli as the unique entry point.
The two missing commands (check-config and submit) have been transfered to checkbox-cli subcommands.
With the removal of checkbox sru, we can also say goodbye to the old textland bits and drop all sru specific options from the config module.
- checkbox-cli as a launcher tested with 16.04 certification test plan.
- check-config ok (properly see ~/.config/
- submit ok (tested both xml and tar.xz with both prod and staging endpoints - only staging for the submission service).
PLEASE DO NOT MERGE UNTIL SRU TOOLS ARE UPDATED IN THE LAB. (Contact: Tai)
Taihsiang Ho (tai271828) wrote : | # |
SRU tools is ready to merge. Please refer to:
https:/
https:/
Taihsiang Ho (tai271828) wrote : | # |
It should be this https:/
Preview Diff
1 | diff --git a/checkbox_ng/commands/__init__.py b/checkbox_ng/commands/__init__.py | |||
2 | 0 | deleted file mode 100644 | 0 | deleted file mode 100644 |
3 | index a7ea4d3..0000000 | |||
4 | --- a/checkbox_ng/commands/__init__.py | |||
5 | +++ /dev/null | |||
6 | @@ -1,63 +0,0 @@ | |||
7 | 1 | # This file is part of Checkbox. | ||
8 | 2 | # | ||
9 | 3 | # Copyright 2014 Canonical Ltd. | ||
10 | 4 | # Written by: | ||
11 | 5 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
12 | 6 | # | ||
13 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
14 | 8 | # it under the terms of the GNU General Public License version 3, | ||
15 | 9 | # as published by the Free Software Foundation. | ||
16 | 10 | # | ||
17 | 11 | # Checkbox is distributed in the hope that it will be useful, | ||
18 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
19 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
20 | 14 | # GNU General Public License for more details. | ||
21 | 15 | # | ||
22 | 16 | # You should have received a copy of the GNU General Public License | ||
23 | 17 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
24 | 18 | |||
25 | 19 | """ | ||
26 | 20 | :mod:`checkbox_ng.commands` -- shared code for checkbox-ng sub-commands | ||
27 | 21 | ======================================================================= | ||
28 | 22 | """ | ||
29 | 23 | |||
30 | 24 | from plainbox.impl.clitools import CommandBase | ||
31 | 25 | |||
32 | 26 | |||
33 | 27 | class CheckboxCommand(CommandBase): | ||
34 | 28 | """ | ||
35 | 29 | Simple interface class for checkbox-ng commands. | ||
36 | 30 | |||
37 | 31 | Command objects like this are consumed by CheckBoxNGTool subclasses to | ||
38 | 32 | implement hierarchical command system. The API supports arbitrary many sub | ||
39 | 33 | commands in arbitrary nesting arrangement. | ||
40 | 34 | """ | ||
41 | 35 | |||
42 | 36 | gettext_domain = "checkbox-ng" | ||
43 | 37 | |||
44 | 38 | def __init__(self, provider_loader, config_loader): | ||
45 | 39 | """ | ||
46 | 40 | Initialize a command with the specified arguments. | ||
47 | 41 | |||
48 | 42 | :param provider_loader: | ||
49 | 43 | A callable returning a list of Provider1 objects | ||
50 | 44 | :param config_loader: | ||
51 | 45 | A callable returning a Config object | ||
52 | 46 | """ | ||
53 | 47 | self._provider_loader = provider_loader | ||
54 | 48 | self._config_loader = config_loader | ||
55 | 49 | |||
56 | 50 | @property | ||
57 | 51 | def provider_loader(self): | ||
58 | 52 | """ | ||
59 | 53 | a callable returning a list of PlainBox providers associated with this | ||
60 | 54 | command | ||
61 | 55 | """ | ||
62 | 56 | return self._provider_loader | ||
63 | 57 | |||
64 | 58 | @property | ||
65 | 59 | def config_loader(self): | ||
66 | 60 | """ | ||
67 | 61 | a callable returning a Config object | ||
68 | 62 | """ | ||
69 | 63 | return self._config_loader | ||
70 | diff --git a/checkbox_ng/commands/cli.py b/checkbox_ng/commands/cli.py | |||
71 | 64 | deleted file mode 100644 | 0 | deleted file mode 100644 |
72 | index e7d6c45..0000000 | |||
73 | --- a/checkbox_ng/commands/cli.py | |||
74 | +++ /dev/null | |||
75 | @@ -1,82 +0,0 @@ | |||
76 | 1 | # This file is part of Checkbox. | ||
77 | 2 | # | ||
78 | 3 | # Copyright 2013-2014 Canonical Ltd. | ||
79 | 4 | # Written by: | ||
80 | 5 | # Sylvain Pineau <sylvain.pineau@canonical.com> | ||
81 | 6 | # | ||
82 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
83 | 8 | # it under the terms of the GNU General Public License version 3, | ||
84 | 9 | # as published by the Free Software Foundation. | ||
85 | 10 | # | ||
86 | 11 | # Checkbox is distributed in the hope that it will be useful, | ||
87 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
88 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
89 | 14 | # GNU General Public License for more details. | ||
90 | 15 | # | ||
91 | 16 | # You should have received a copy of the GNU General Public License | ||
92 | 17 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
93 | 18 | |||
94 | 19 | """ | ||
95 | 20 | :mod:`checkbox_ng.commands.cli` -- Command line sub-command | ||
96 | 21 | =========================================================== | ||
97 | 22 | |||
98 | 23 | .. warning:: | ||
99 | 24 | |||
100 | 25 | THIS MODULE DOES NOT HAVE STABLE PUBLIC API | ||
101 | 26 | """ | ||
102 | 27 | |||
103 | 28 | from argparse import SUPPRESS | ||
104 | 29 | from gettext import gettext as _ | ||
105 | 30 | from logging import getLogger | ||
106 | 31 | |||
107 | 32 | from plainbox.impl.commands import PlainBoxCommand | ||
108 | 33 | from plainbox.impl.commands.cmd_checkbox import CheckBoxCommandMixIn | ||
109 | 34 | from plainbox.impl.commands.inv_check_config import CheckConfigInvocation | ||
110 | 35 | |||
111 | 36 | from checkbox_ng.commands.newcli import CliInvocation2 | ||
112 | 37 | |||
113 | 38 | |||
114 | 39 | logger = getLogger("checkbox.ng.commands.cli") | ||
115 | 40 | |||
116 | 41 | |||
117 | 42 | class CliCommand(PlainBoxCommand, CheckBoxCommandMixIn): | ||
118 | 43 | """ | ||
119 | 44 | Command for running tests using the command line UI. | ||
120 | 45 | """ | ||
121 | 46 | gettext_domain = "checkbox-ng" | ||
122 | 47 | |||
123 | 48 | def __init__(self, provider_loader, config_loader, settings): | ||
124 | 49 | self.provider_loader = provider_loader | ||
125 | 50 | self.config_loader = config_loader | ||
126 | 51 | self.settings = settings | ||
127 | 52 | |||
128 | 53 | def invoked(self, ns): | ||
129 | 54 | # Run check-config, if requested | ||
130 | 55 | if ns.check_config: | ||
131 | 56 | retval = CheckConfigInvocation(self.config_loader).run() | ||
132 | 57 | return retval | ||
133 | 58 | return CliInvocation2( | ||
134 | 59 | self.provider_loader, self.loader_config, ns, self.settings | ||
135 | 60 | ).run() | ||
136 | 61 | |||
137 | 62 | def register_parser(self, subparsers): | ||
138 | 63 | parser = subparsers.add_parser(self.settings['subparser_name'], | ||
139 | 64 | help=self.settings['subparser_help']) | ||
140 | 65 | parser.set_defaults(command=self) | ||
141 | 66 | parser.set_defaults(dry_run=False) | ||
142 | 67 | parser.add_argument( | ||
143 | 68 | "--check-config", | ||
144 | 69 | action="store_true", | ||
145 | 70 | help=_("run check-config")) | ||
146 | 71 | group = parser.add_argument_group(title=_("user interface options")) | ||
147 | 72 | parser.set_defaults(color=None) | ||
148 | 73 | group.add_argument( | ||
149 | 74 | '--no-color', dest='color', action='store_false', help=SUPPRESS) | ||
150 | 75 | group.add_argument( | ||
151 | 76 | '--non-interactive', action='store_true', | ||
152 | 77 | help=_("skip tests that require interactivity")) | ||
153 | 78 | group.add_argument( | ||
154 | 79 | '--dont-suppress-output', action="store_true", default=False, | ||
155 | 80 | help=_("don't suppress the output of certain job plugin types")) | ||
156 | 81 | # Call enhance_parser from CheckBoxCommandMixIn | ||
157 | 82 | self.enhance_parser(parser) | ||
158 | diff --git a/checkbox_ng/commands/launcher.py b/checkbox_ng/commands/launcher.py | |||
159 | 83 | deleted file mode 100644 | 0 | deleted file mode 100644 |
160 | index 87eb180..0000000 | |||
161 | --- a/checkbox_ng/commands/launcher.py | |||
162 | +++ /dev/null | |||
163 | @@ -1,115 +0,0 @@ | |||
164 | 1 | # This file is part of Checkbox. | ||
165 | 2 | # | ||
166 | 3 | # Copyright 2014 Canonical Ltd. | ||
167 | 4 | # Written by: | ||
168 | 5 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
169 | 6 | # | ||
170 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
171 | 8 | # it under the terms of the GNU General Public License version 3, | ||
172 | 9 | # as published by the Free Software Foundation. | ||
173 | 10 | |||
174 | 11 | # | ||
175 | 12 | # Checkbox is distributed in the hope that it will be useful, | ||
176 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
177 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
178 | 15 | # GNU General Public License for more details. | ||
179 | 16 | # | ||
180 | 17 | # You should have received a copy of the GNU General Public License | ||
181 | 18 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
182 | 19 | |||
183 | 20 | """ | ||
184 | 21 | :mod:`checkbox_ng.commands.launcher` -- `checkbox launcher` command | ||
185 | 22 | =================================================================== | ||
186 | 23 | """ | ||
187 | 24 | |||
188 | 25 | from argparse import SUPPRESS | ||
189 | 26 | from gettext import gettext as _ | ||
190 | 27 | import itertools | ||
191 | 28 | import logging | ||
192 | 29 | import os | ||
193 | 30 | |||
194 | 31 | from checkbox_ng.commands import CheckboxCommand | ||
195 | 32 | from checkbox_ng.commands.newcli import CliInvocation2 | ||
196 | 33 | from checkbox_ng.commands.submit import SubmitCommand | ||
197 | 34 | from checkbox_ng.config import CheckBoxConfig | ||
198 | 35 | |||
199 | 36 | from plainbox.impl.commands.cmd_checkbox import CheckBoxCommandMixIn | ||
200 | 37 | from plainbox.impl.launcher import LauncherDefinition | ||
201 | 38 | |||
202 | 39 | logger = logging.getLogger("checkbox.ng.commands.launcher") | ||
203 | 40 | |||
204 | 41 | |||
205 | 42 | class LauncherCommand(CheckboxCommand, CheckBoxCommandMixIn, SubmitCommand): | ||
206 | 43 | """ | ||
207 | 44 | run a customized testing session | ||
208 | 45 | |||
209 | 46 | This command can be used as an interpreter for the so-called launchers. | ||
210 | 47 | Those launchers are small text files that define the parameters of the test | ||
211 | 48 | and can be executed directly to run a customized checkbox-ng testing | ||
212 | 49 | session. | ||
213 | 50 | """ | ||
214 | 51 | |||
215 | 52 | def __init__(self, provider_loader, config_loader): | ||
216 | 53 | self._provider_loader = provider_loader | ||
217 | 54 | self.config = config_loader() | ||
218 | 55 | |||
219 | 56 | def invoked(self, ns): | ||
220 | 57 | try: | ||
221 | 58 | with open(ns.launcher, 'rt', encoding='UTF-8') as stream: | ||
222 | 59 | first_line = stream.readline() | ||
223 | 60 | if not first_line.startswith("#!"): | ||
224 | 61 | stream.seek(0) | ||
225 | 62 | text = stream.read() | ||
226 | 63 | except IOError as exc: | ||
227 | 64 | logger.error(_("Unable to load launcher definition: %s"), exc) | ||
228 | 65 | return 1 | ||
229 | 66 | generic_launcher = LauncherDefinition() | ||
230 | 67 | generic_launcher.read_string(text) | ||
231 | 68 | launcher = generic_launcher.get_concrete_launcher() | ||
232 | 69 | launcher.read_string(text) | ||
233 | 70 | if launcher.problem_list: | ||
234 | 71 | logger.error(_("Unable to start launcher because of errors:")) | ||
235 | 72 | for problem in launcher.problem_list: | ||
236 | 73 | logger.error("%s", str(problem)) | ||
237 | 74 | return 1 | ||
238 | 75 | # Override the default CheckBox configuration with the one provided | ||
239 | 76 | # by the launcher | ||
240 | 77 | self.config.Meta.filename_list = list( | ||
241 | 78 | itertools.chain( | ||
242 | 79 | *zip( | ||
243 | 80 | itertools.islice( | ||
244 | 81 | CheckBoxConfig.Meta.filename_list, 0, None, 2), | ||
245 | 82 | itertools.islice( | ||
246 | 83 | CheckBoxConfig.Meta.filename_list, 1, None, 2), | ||
247 | 84 | ('/etc/xdg/{}'.format(launcher.config_filename), | ||
248 | 85 | os.path.expanduser( | ||
249 | 86 | '~/.config/{}'.format(launcher.config_filename))))) | ||
250 | 87 | ) | ||
251 | 88 | self.config.read(self.config.Meta.filename_list) | ||
252 | 89 | ns.dry_run = False | ||
253 | 90 | ns.dont_suppress_output = launcher.dont_suppress_output | ||
254 | 91 | return CliInvocation2( | ||
255 | 92 | self.provider_loader, lambda: self.config, ns, launcher | ||
256 | 93 | ).run() | ||
257 | 94 | |||
258 | 95 | def register_parser(self, subparsers): | ||
259 | 96 | parser = self.add_subcommand(subparsers) | ||
260 | 97 | self.register_arguments(parser) | ||
261 | 98 | |||
262 | 99 | def register_arguments(self, parser): | ||
263 | 100 | parser.add_argument( | ||
264 | 101 | '--no-color', dest='color', action='store_false', help=SUPPRESS) | ||
265 | 102 | parser.set_defaults(color=None) | ||
266 | 103 | parser.add_argument( | ||
267 | 104 | "launcher", metavar=_("LAUNCHER"), | ||
268 | 105 | help=_("launcher definition file to use")) | ||
269 | 106 | parser.set_defaults(command=self) | ||
270 | 107 | parser.conflict_handler = 'resolve' | ||
271 | 108 | # Call enhance_parser from CheckBoxCommandMixIn | ||
272 | 109 | self.enhance_parser(parser) | ||
273 | 110 | group = parser.add_argument_group(title=_("user interface options")) | ||
274 | 111 | group.add_argument( | ||
275 | 112 | '--non-interactive', action='store_true', | ||
276 | 113 | help=_("skip tests that require interactivity")) | ||
277 | 114 | # Call register_optional_arguments from SubmitCommand | ||
278 | 115 | self.register_optional_arguments(parser) | ||
279 | diff --git a/checkbox_ng/commands/newcli.py b/checkbox_ng/commands/newcli.py | |||
280 | 116 | deleted file mode 100644 | 0 | deleted file mode 100644 |
281 | index 8d359c0..0000000 | |||
282 | --- a/checkbox_ng/commands/newcli.py | |||
283 | +++ /dev/null | |||
284 | @@ -1,491 +0,0 @@ | |||
285 | 1 | # This file is part of Checkbox. | ||
286 | 2 | # | ||
287 | 3 | # Copyright 2013-2014 Canonical Ltd. | ||
288 | 4 | # Written by: | ||
289 | 5 | # Sylvain Pineau <sylvain.pineau@canonical.com> | ||
290 | 6 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
291 | 7 | # | ||
292 | 8 | # Checkbox is free software: you can redistribute it and/or modify | ||
293 | 9 | # it under the terms of the GNU General Public License version 3, | ||
294 | 10 | # as published by the Free Software Foundation. | ||
295 | 11 | # | ||
296 | 12 | # Checkbox is distributed in the hope that it will be useful, | ||
297 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
298 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
299 | 15 | # GNU General Public License for more details. | ||
300 | 16 | # | ||
301 | 17 | # You should have received a copy of the GNU General Public License | ||
302 | 18 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
303 | 19 | |||
304 | 20 | """ | ||
305 | 21 | :mod:`checkbox_ng.commands.cli` -- Command line sub-command | ||
306 | 22 | =========================================================== | ||
307 | 23 | |||
308 | 24 | .. warning:: | ||
309 | 25 | |||
310 | 26 | THIS MODULE DOES NOT HAVE STABLE PUBLIC API | ||
311 | 27 | """ | ||
312 | 28 | |||
313 | 29 | from gettext import gettext as _ | ||
314 | 30 | from logging import getLogger | ||
315 | 31 | from shutil import copyfileobj | ||
316 | 32 | import io | ||
317 | 33 | import operator | ||
318 | 34 | import os | ||
319 | 35 | import re | ||
320 | 36 | import subprocess | ||
321 | 37 | import sys | ||
322 | 38 | |||
323 | 39 | from plainbox.abc import IJobResult | ||
324 | 40 | from plainbox.impl.commands.inv_run import RunInvocation | ||
325 | 41 | from plainbox.impl.exporter import ByteStringStreamTranslator | ||
326 | 42 | from plainbox.impl.secure.config import Unset, ValidationError | ||
327 | 43 | from plainbox.impl.secure.origin import CommandLineTextSource | ||
328 | 44 | from plainbox.impl.secure.origin import Origin | ||
329 | 45 | from plainbox.impl.secure.qualifiers import FieldQualifier | ||
330 | 46 | from plainbox.impl.secure.qualifiers import OperatorMatcher | ||
331 | 47 | from plainbox.impl.secure.qualifiers import RegExpJobQualifier | ||
332 | 48 | from plainbox.impl.secure.qualifiers import select_jobs | ||
333 | 49 | from plainbox.impl.session import SessionMetaData | ||
334 | 50 | from plainbox.impl.session.jobs import InhibitionCause | ||
335 | 51 | from plainbox.impl.transport import TransportError | ||
336 | 52 | from plainbox.impl.transport import get_all_transports | ||
337 | 53 | from plainbox.vendor.textland import get_display | ||
338 | 54 | |||
339 | 55 | from checkbox_ng.misc import SelectableJobTreeNode | ||
340 | 56 | from checkbox_ng.ui import ScrollableTreeNode | ||
341 | 57 | from checkbox_ng.ui import ShowMenu | ||
342 | 58 | from checkbox_ng.ui import ShowRerun | ||
343 | 59 | from checkbox_ng.ui import ShowWelcome | ||
344 | 60 | |||
345 | 61 | |||
346 | 62 | logger = getLogger("checkbox.ng.commands.newcli") | ||
347 | 63 | |||
348 | 64 | |||
349 | 65 | class CliInvocation2(RunInvocation): | ||
350 | 66 | """ | ||
351 | 67 | Invocation of the 'checkbox cli' command. | ||
352 | 68 | |||
353 | 69 | :ivar ns: | ||
354 | 70 | The argparse namespace obtained from CliCommand | ||
355 | 71 | :ivar _launcher: | ||
356 | 72 | launcher specific to 'checkbox cli' | ||
357 | 73 | :ivar _display: | ||
358 | 74 | A textland display object | ||
359 | 75 | :ivar _qualifier_list: | ||
360 | 76 | A list of job qualifiers used to build the session desired_job_list | ||
361 | 77 | """ | ||
362 | 78 | |||
363 | 79 | def __init__(self, provider_loader, config_loader, ns, launcher, | ||
364 | 80 | display=None): | ||
365 | 81 | super().__init__(provider_loader, config_loader, ns, ns.color) | ||
366 | 82 | if display is None: | ||
367 | 83 | display = get_display() | ||
368 | 84 | self._launcher = launcher | ||
369 | 85 | self._display = display | ||
370 | 86 | self._qualifier_list = [] | ||
371 | 87 | self._testplan_list = [] | ||
372 | 88 | self.select_qualifier_list() | ||
373 | 89 | # MAAS-deployed server images need "tput reset" to keep ugliness | ||
374 | 90 | # from happening.... | ||
375 | 91 | subprocess.check_call(['tput', 'reset']) | ||
376 | 92 | |||
377 | 93 | @property | ||
378 | 94 | def launcher(self): | ||
379 | 95 | """ | ||
380 | 96 | TBD: 'checkbox cli' specific launcher settings | ||
381 | 97 | """ | ||
382 | 98 | return self._launcher | ||
383 | 99 | |||
384 | 100 | @property | ||
385 | 101 | def display(self): | ||
386 | 102 | """ | ||
387 | 103 | A TextLand display object | ||
388 | 104 | """ | ||
389 | 105 | return self._display | ||
390 | 106 | |||
391 | 107 | def select_qualifier_list(self): | ||
392 | 108 | # Add whitelists | ||
393 | 109 | if 'whitelist' in self.ns and self.ns.whitelist: | ||
394 | 110 | for whitelist_file in self.ns.whitelist: | ||
395 | 111 | qualifier = self.get_whitelist_from_file( | ||
396 | 112 | whitelist_file.name, whitelist_file) | ||
397 | 113 | if qualifier is not None: | ||
398 | 114 | self._qualifier_list.append(qualifier) | ||
399 | 115 | # Add all the --include jobs | ||
400 | 116 | for pattern in self.ns.include_pattern_list: | ||
401 | 117 | origin = Origin(CommandLineTextSource('-i', pattern), None, None) | ||
402 | 118 | try: | ||
403 | 119 | qualifier = RegExpJobQualifier( | ||
404 | 120 | '^{}$'.format(pattern), origin, inclusive=True) | ||
405 | 121 | except Exception as exc: | ||
406 | 122 | logger.warning( | ||
407 | 123 | _("Incorrect pattern %r: %s"), pattern, exc) | ||
408 | 124 | else: | ||
409 | 125 | self._qualifier_list.append(qualifier) | ||
410 | 126 | # Add all the --exclude jobs | ||
411 | 127 | for pattern in self.ns.exclude_pattern_list: | ||
412 | 128 | origin = Origin(CommandLineTextSource('-x', pattern), None, None) | ||
413 | 129 | try: | ||
414 | 130 | qualifier = RegExpJobQualifier( | ||
415 | 131 | '^{}$'.format(pattern), origin, inclusive=False) | ||
416 | 132 | except Exception as exc: | ||
417 | 133 | logger.warning( | ||
418 | 134 | _("Incorrect pattern %r: %s"), pattern, exc) | ||
419 | 135 | else: | ||
420 | 136 | self._qualifier_list.append(qualifier) | ||
421 | 137 | if self.config.whitelist is not Unset: | ||
422 | 138 | self._qualifier_list.append( | ||
423 | 139 | self.get_whitelist_from_file(self.config.whitelist)) | ||
424 | 140 | |||
425 | 141 | def select_testplan(self): | ||
426 | 142 | # Add the test plan | ||
427 | 143 | if self.ns.test_plan is not None: | ||
428 | 144 | for provider in self.provider_list: | ||
429 | 145 | for unit in provider.id_map[self.ns.test_plan]: | ||
430 | 146 | if unit.Meta.name == 'test plan': | ||
431 | 147 | self._qualifier_list.append(unit.get_qualifier()) | ||
432 | 148 | self._testplan_list.append(unit) | ||
433 | 149 | return | ||
434 | 150 | else: | ||
435 | 151 | logger.error(_("There is no test plan: %s"), self.ns.test_plan) | ||
436 | 152 | |||
437 | 153 | def run(self): | ||
438 | 154 | return self.do_normal_sequence() | ||
439 | 155 | |||
440 | 156 | def do_normal_sequence(self): | ||
441 | 157 | """ | ||
442 | 158 | Proceed through normal set of steps that are required to runs jobs | ||
443 | 159 | |||
444 | 160 | .. note:: | ||
445 | 161 | This version is overridden as there is no better way to manage this | ||
446 | 162 | pile rather than having a copy-paste + edits piece of text until | ||
447 | 163 | arrowhead replaced plainbox run internals with a flow chart that | ||
448 | 164 | can be derived meaningfully. | ||
449 | 165 | |||
450 | 166 | For now just look for changes as compared to run.py's version. | ||
451 | 167 | """ | ||
452 | 168 | # Create transport early so that we can handle bugs before starting the | ||
453 | 169 | # session. | ||
454 | 170 | self.create_transport() | ||
455 | 171 | if self.is_interactive: | ||
456 | 172 | resumed = self.maybe_resume_session() | ||
457 | 173 | else: | ||
458 | 174 | self.create_manager(None) | ||
459 | 175 | resumed = False | ||
460 | 176 | # XXX: we don't want to know about new jobs just yet | ||
461 | 177 | self.state.on_job_added.disconnect(self.on_job_added) | ||
462 | 178 | # Create the job runner so that we can do stuff | ||
463 | 179 | self.create_runner() | ||
464 | 180 | # If we haven't resumed then do some one-time initialization | ||
465 | 181 | if not resumed: | ||
466 | 182 | # Show the welcome message | ||
467 | 183 | self.show_welcome_screen() | ||
468 | 184 | # Process testplan command line options | ||
469 | 185 | self.select_testplan() | ||
470 | 186 | # Maybe allow the user to do a manual whitelist selection | ||
471 | 187 | if not self._qualifier_list: | ||
472 | 188 | self.maybe_interactively_select_testplans() | ||
473 | 189 | if self._testplan_list: | ||
474 | 190 | self.manager.test_plans = tuple(self._testplan_list) | ||
475 | 191 | # Store the application-identifying meta-data and checkpoint the | ||
476 | 192 | # session. | ||
477 | 193 | self.store_application_metadata() | ||
478 | 194 | self.metadata.flags.add(SessionMetaData.FLAG_INCOMPLETE) | ||
479 | 195 | self.manager.checkpoint() | ||
480 | 196 | # Run all the local jobs. We need to do this to see all the things | ||
481 | 197 | # the user may select | ||
482 | 198 | if self.is_interactive: | ||
483 | 199 | self.select_local_jobs() | ||
484 | 200 | self.run_all_selected_jobs() | ||
485 | 201 | self.interactively_pick_jobs_to_run() | ||
486 | 202 | # Maybe ask the secure launcher to prompt for the password now. This is | ||
487 | 203 | # imperfect as we are going to run local jobs and we cannot see if they | ||
488 | 204 | # might need root or not. This cannot be fixed before template jobs are | ||
489 | 205 | # added and local jobs deprecated and removed (at least not being a | ||
490 | 206 | # part of the session we want to execute). | ||
491 | 207 | self.maybe_warm_up_authentication() | ||
492 | 208 | self.print_estimated_duration() | ||
493 | 209 | self.run_all_selected_jobs() | ||
494 | 210 | if self.is_interactive: | ||
495 | 211 | while True: | ||
496 | 212 | if self.maybe_rerun_jobs(): | ||
497 | 213 | continue | ||
498 | 214 | else: | ||
499 | 215 | break | ||
500 | 216 | self.export_and_send_results() | ||
501 | 217 | if SessionMetaData.FLAG_INCOMPLETE in self.metadata.flags: | ||
502 | 218 | print(self.C.header("Session Complete!", "GREEN")) | ||
503 | 219 | self.metadata.flags.remove(SessionMetaData.FLAG_INCOMPLETE) | ||
504 | 220 | self.manager.checkpoint() | ||
505 | 221 | return 0 | ||
506 | 222 | |||
507 | 223 | def store_application_metadata(self): | ||
508 | 224 | super().store_application_metadata() | ||
509 | 225 | self.metadata.app_blob = b'' | ||
510 | 226 | |||
511 | 227 | def show_welcome_screen(self): | ||
512 | 228 | text = self.launcher.text | ||
513 | 229 | if self.is_interactive and text: | ||
514 | 230 | self.display.run(ShowWelcome(text)) | ||
515 | 231 | |||
516 | 232 | def maybe_interactively_select_testplans(self): | ||
517 | 233 | if self.launcher.skip_whitelist_selection: | ||
518 | 234 | self._qualifier_list.extend(self.get_default_testplans()) | ||
519 | 235 | elif self.is_interactive: | ||
520 | 236 | self._qualifier_list.extend( | ||
521 | 237 | self.get_interactively_picked_testplans()) | ||
522 | 238 | elif self.launcher.whitelist_selection: | ||
523 | 239 | self._qualifier_list.extend(self.get_default_testplans()) | ||
524 | 240 | logger.info(_("Selected testplans: %r"), self._qualifier_list) | ||
525 | 241 | |||
526 | 242 | def get_interactively_picked_testplans(self): | ||
527 | 243 | """ | ||
528 | 244 | Show an interactive dialog that allows the user to pick a list of | ||
529 | 245 | testplans. The set of testplans is limited to those offered by the | ||
530 | 246 | 'default_providers' setting. | ||
531 | 247 | |||
532 | 248 | :returns: | ||
533 | 249 | A list of selected testplans | ||
534 | 250 | """ | ||
535 | 251 | testplans = [] | ||
536 | 252 | testplan_selection = [] | ||
537 | 253 | for provider in self.provider_list: | ||
538 | 254 | testplans.extend( | ||
539 | 255 | [unit for unit in provider.unit_list if | ||
540 | 256 | unit.Meta.name == 'test plan' and | ||
541 | 257 | re.search(self.launcher.whitelist_filter, unit.partial_id)]) | ||
542 | 258 | testplan_name_list = [testplan.tr_name() for testplan in testplans] | ||
543 | 259 | testplan_selection = [ | ||
544 | 260 | testplans.index(t) for t in testplans if | ||
545 | 261 | re.search(self.launcher.whitelist_selection, t.partial_id)] | ||
546 | 262 | selected_list = self.display.run( | ||
547 | 263 | ShowMenu(_("Suite selection"), testplan_name_list, | ||
548 | 264 | testplan_selection)) | ||
549 | 265 | if not selected_list: | ||
550 | 266 | raise SystemExit(_("No testplan selected, aborting")) | ||
551 | 267 | self._testplan_list.extend( | ||
552 | 268 | [testplans[selected_index] for selected_index in selected_list]) | ||
553 | 269 | return [testplans[selected_index].get_qualifier() for selected_index | ||
554 | 270 | in selected_list] | ||
555 | 271 | |||
556 | 272 | def get_default_testplans(self): | ||
557 | 273 | testplans = [] | ||
558 | 274 | for provider in self.provider_list: | ||
559 | 275 | testplans.extend([ | ||
560 | 276 | unit.get_qualifier() for unit in provider.unit_list if | ||
561 | 277 | unit.Meta.name == 'test plan' and re.search( | ||
562 | 278 | self.launcher.whitelist_selection, unit.partial_id)]) | ||
563 | 279 | return testplans | ||
564 | 280 | |||
565 | 281 | def create_transport(self): | ||
566 | 282 | """ | ||
567 | 283 | Create the ISessionStateTransport based on the command line options | ||
568 | 284 | |||
569 | 285 | This sets the :ivar:`_transport`. | ||
570 | 286 | """ | ||
571 | 287 | # TODO: | ||
572 | 288 | self._transport = None | ||
573 | 289 | |||
574 | 290 | @property | ||
575 | 291 | def expected_app_id(self): | ||
576 | 292 | return 'checkbox' | ||
577 | 293 | |||
578 | 294 | def select_local_jobs(self): | ||
579 | 295 | print(self.C.header(_("Selecting Job Generators"))) | ||
580 | 296 | # Create a qualifier list that will pick all local jobs out of the | ||
581 | 297 | # subset of jobs also enumerated by the whitelists we've already | ||
582 | 298 | # picked. | ||
583 | 299 | # | ||
584 | 300 | # Since each whitelist is a qualifier that selects jobs enumerated | ||
585 | 301 | # within, we only need to and an exclusive qualifier that deselects | ||
586 | 302 | # non-local jobs and we're done. | ||
587 | 303 | qualifier_list = [] | ||
588 | 304 | qualifier_list.extend(self._qualifier_list) | ||
589 | 305 | origin = Origin.get_caller_origin() | ||
590 | 306 | qualifier_list.append(FieldQualifier( | ||
591 | 307 | 'plugin', OperatorMatcher(operator.ne, 'local'), origin, | ||
592 | 308 | inclusive=False)) | ||
593 | 309 | local_job_list = select_jobs( | ||
594 | 310 | self.manager.state.job_list, qualifier_list) | ||
595 | 311 | self._update_desired_job_list(local_job_list) | ||
596 | 312 | |||
597 | 313 | def interactively_pick_jobs_to_run(self): | ||
598 | 314 | print(self.C.header(_("Selecting Jobs For Execution"))) | ||
599 | 315 | self._update_desired_job_list(select_jobs( | ||
600 | 316 | self.manager.state.job_list, self._qualifier_list)) | ||
601 | 317 | if self.launcher.skip_test_selection or not self.is_interactive: | ||
602 | 318 | return | ||
603 | 319 | tree = SelectableJobTreeNode.create_tree( | ||
604 | 320 | self.manager.state, self.manager.state.run_list) | ||
605 | 321 | title = _('Choose tests to run on your system:') | ||
606 | 322 | self.display.run(ScrollableTreeNode(tree, title)) | ||
607 | 323 | # NOTE: tree.selection is correct but ordered badly. To retain | ||
608 | 324 | # the original ordering we should just treat it as a mask and | ||
609 | 325 | # use it to filter jobs from desired_job_list. | ||
610 | 326 | wanted_set = frozenset(tree.selection + tree.resource_jobs) | ||
611 | 327 | job_list = [job for job in self.manager.state.run_list | ||
612 | 328 | if job in wanted_set] | ||
613 | 329 | self._update_desired_job_list(job_list) | ||
614 | 330 | |||
615 | 331 | def export_and_send_results(self): | ||
616 | 332 | if self.is_interactive: | ||
617 | 333 | print(self.C.header(_("Results"))) | ||
618 | 334 | exporter = self.manager.create_exporter( | ||
619 | 335 | '2013.com.canonical.plainbox::text') | ||
620 | 336 | exported_stream = io.BytesIO() | ||
621 | 337 | exporter.dump_from_session_manager(self.manager, exported_stream) | ||
622 | 338 | exported_stream.seek(0) # Need to rewind the file, puagh | ||
623 | 339 | # This requires a bit more finesse, as exporters output bytes | ||
624 | 340 | # and stdout needs a string. | ||
625 | 341 | translating_stream = ByteStringStreamTranslator( | ||
626 | 342 | sys.stdout, "utf-8") | ||
627 | 343 | copyfileobj(exported_stream, translating_stream) | ||
628 | 344 | # FIXME: this should probably not go to plainbox but checkbox-ng | ||
629 | 345 | base_dir = os.path.join( | ||
630 | 346 | os.getenv( | ||
631 | 347 | 'XDG_DATA_HOME', os.path.expanduser("~/.local/share/")), | ||
632 | 348 | "plainbox") | ||
633 | 349 | if not os.path.exists(base_dir): | ||
634 | 350 | os.makedirs(base_dir) | ||
635 | 351 | exp_options = ['with-sys-info', 'with-summary', 'with-job-description', | ||
636 | 352 | 'with-text-attachments', 'with-certification-status', | ||
637 | 353 | 'with-job-defs', 'with-io-log', 'with-comments'] | ||
638 | 354 | print() | ||
639 | 355 | if self.launcher.exporter is not Unset: | ||
640 | 356 | exporters = self.launcher.exporter | ||
641 | 357 | else: | ||
642 | 358 | exporters = [ | ||
643 | 359 | '2013.com.canonical.plainbox::hexr', | ||
644 | 360 | '2013.com.canonical.plainbox::html', | ||
645 | 361 | '2013.com.canonical.plainbox::xlsx', | ||
646 | 362 | '2013.com.canonical.plainbox::json', | ||
647 | 363 | ] | ||
648 | 364 | for unit_name in exporters: | ||
649 | 365 | exporter = self.manager.create_exporter( | ||
650 | 366 | unit_name, exp_options, strict=False) | ||
651 | 367 | extension = exporter.unit.file_extension | ||
652 | 368 | results_path = os.path.join( | ||
653 | 369 | base_dir, 'submission.{}'.format(extension)) | ||
654 | 370 | with open(results_path, "wb") as stream: | ||
655 | 371 | exporter.dump_from_session_manager(self.manager, stream) | ||
656 | 372 | print(_("View results") + " ({}): file://{}".format( | ||
657 | 373 | extension, results_path)) | ||
658 | 374 | self.submission_file = os.path.join(base_dir, 'submission.xml') | ||
659 | 375 | if self.launcher.submit_to is not Unset: | ||
660 | 376 | if self.launcher.submit_to == 'certification': | ||
661 | 377 | # If we supplied a submit_url in the launcher, it | ||
662 | 378 | # should override the one in the config. | ||
663 | 379 | if self.launcher.submit_url: | ||
664 | 380 | self.config.c3_url = self.launcher.submit_url | ||
665 | 381 | # Same behavior for submit_to_hexr (a boolean flag which | ||
666 | 382 | # should result in adding "submit_to_hexr=1" to transport | ||
667 | 383 | # options later on) | ||
668 | 384 | if self.launcher.submit_to_hexr: | ||
669 | 385 | self.config.submit_to_hexr = True | ||
670 | 386 | # for secure_id, config (which is user-writable) should | ||
671 | 387 | # override launcher (which is not) | ||
672 | 388 | if not self.config.secure_id: | ||
673 | 389 | self.config.secure_id = self.launcher.secure_id | ||
674 | 390 | # Override the secure_id configuration with the one provided | ||
675 | 391 | # by the command-line option | ||
676 | 392 | if self.ns.secure_id: | ||
677 | 393 | self.config.secure_id = self.ns.secure_id | ||
678 | 394 | if self.config.secure_id is Unset: | ||
679 | 395 | again = True | ||
680 | 396 | if not self.is_interactive: | ||
681 | 397 | again = False | ||
682 | 398 | while again: | ||
683 | 399 | # TRANSLATORS: Do not translate the {} format marker. | ||
684 | 400 | if self.ask_for_confirmation( | ||
685 | 401 | _("\nSubmit results to {0}?".format( | ||
686 | 402 | self.launcher.submit_url))): | ||
687 | 403 | try: | ||
688 | 404 | self.config.secure_id = input(_("Secure ID: ")) | ||
689 | 405 | except ValidationError: | ||
690 | 406 | print( | ||
691 | 407 | _("ERROR: Secure ID must be 15-character " | ||
692 | 408 | "(or more) alphanumeric string")) | ||
693 | 409 | else: | ||
694 | 410 | again = False | ||
695 | 411 | self.submit_certification_results() | ||
696 | 412 | else: | ||
697 | 413 | again = False | ||
698 | 414 | else: | ||
699 | 415 | # Automatically try to submit results if the secure_id is | ||
700 | 416 | # valid | ||
701 | 417 | self.submit_certification_results() | ||
702 | 418 | |||
703 | 419 | def submit_certification_results(self): | ||
704 | 420 | from checkbox_ng.certification import InvalidSecureIDError | ||
705 | 421 | transport_cls = get_all_transports().get('certification') | ||
706 | 422 | # TRANSLATORS: Do not translate the {} format markers. | ||
707 | 423 | print(_("Submitting results to {0} for secure_id {1}").format( | ||
708 | 424 | self.config.c3_url, self.config.secure_id)) | ||
709 | 425 | option_chunks = [] | ||
710 | 426 | option_chunks.append("secure_id={0}".format(self.config.secure_id)) | ||
711 | 427 | if self.config.submit_to_hexr: | ||
712 | 428 | option_chunks.append("submit_to_hexr=1") | ||
713 | 429 | # Assemble the option string | ||
714 | 430 | options_string = ",".join(option_chunks) | ||
715 | 431 | # Create the transport object | ||
716 | 432 | try: | ||
717 | 433 | transport = transport_cls( | ||
718 | 434 | self.config.c3_url, options_string) | ||
719 | 435 | except InvalidSecureIDError as exc: | ||
720 | 436 | print(exc) | ||
721 | 437 | return False | ||
722 | 438 | with open(self.submission_file, "r", encoding='utf-8') as stream: | ||
723 | 439 | try: | ||
724 | 440 | # Send the data, reading from the fallback file | ||
725 | 441 | result = transport.send(stream, self.config) | ||
726 | 442 | if 'url' in result: | ||
727 | 443 | # TRANSLATORS: Do not translate the {} format marker. | ||
728 | 444 | print(_("Successfully sent, submission status" | ||
729 | 445 | " at {0}").format(result['url'])) | ||
730 | 446 | else: | ||
731 | 447 | # TRANSLATORS: Do not translate the {} format marker. | ||
732 | 448 | print(_("Successfully sent, server response" | ||
733 | 449 | ": {0}").format(result)) | ||
734 | 450 | except TransportError as exc: | ||
735 | 451 | print(str(exc)) | ||
736 | 452 | |||
737 | 453 | def maybe_rerun_jobs(self): | ||
738 | 454 | # create a list of jobs that qualify for rerunning | ||
739 | 455 | rerun_candidates = [] | ||
740 | 456 | for job in self.manager.state.run_list: | ||
741 | 457 | job_state = self.manager.state.job_state_map[job.id] | ||
742 | 458 | if job_state.result.outcome in ( | ||
743 | 459 | IJobResult.OUTCOME_FAIL, IJobResult.OUTCOME_CRASH, | ||
744 | 460 | IJobResult.OUTCOME_NOT_SUPPORTED): | ||
745 | 461 | rerun_candidates.append(job) | ||
746 | 462 | |||
747 | 463 | # bail-out early if no job qualifies for rerunning | ||
748 | 464 | if not rerun_candidates: | ||
749 | 465 | return False | ||
750 | 466 | tree = SelectableJobTreeNode.create_tree( | ||
751 | 467 | self.manager.state, rerun_candidates) | ||
752 | 468 | # nothing to select in root node and categories - bailing out | ||
753 | 469 | if not tree.jobs and not tree._categories: | ||
754 | 470 | return False | ||
755 | 471 | # deselect all by default | ||
756 | 472 | tree.set_descendants_state(False) | ||
757 | 473 | self.display.run(ShowRerun(tree, _("Select jobs to re-run"))) | ||
758 | 474 | wanted_set = frozenset(tree.selection) | ||
759 | 475 | if not wanted_set: | ||
760 | 476 | # nothing selected - nothing to run | ||
761 | 477 | return False | ||
762 | 478 | # include resource jobs that selected jobs depend on | ||
763 | 479 | resources_to_rerun = [] | ||
764 | 480 | for job in wanted_set: | ||
765 | 481 | job_state = self.manager.state.job_state_map[job.id] | ||
766 | 482 | for inhibitor in job_state.readiness_inhibitor_list: | ||
767 | 483 | if inhibitor.cause == InhibitionCause.FAILED_DEP: | ||
768 | 484 | resources_to_rerun.append(inhibitor.related_job) | ||
769 | 485 | # reset outcome of jobs that are selected for re-running | ||
770 | 486 | for job in list(wanted_set) + resources_to_rerun: | ||
771 | 487 | from plainbox.impl.result import MemoryJobResult | ||
772 | 488 | self.manager.state.job_state_map[job.id].result = \ | ||
773 | 489 | MemoryJobResult({}) | ||
774 | 490 | self.run_all_selected_jobs() | ||
775 | 491 | return True | ||
776 | diff --git a/checkbox_ng/commands/sru.py b/checkbox_ng/commands/sru.py | |||
777 | 492 | deleted file mode 100644 | 0 | deleted file mode 100644 |
778 | index 7db7b71..0000000 | |||
779 | --- a/checkbox_ng/commands/sru.py | |||
780 | +++ /dev/null | |||
781 | @@ -1,174 +0,0 @@ | |||
782 | 1 | # This file is part of Checkbox. | ||
783 | 2 | # | ||
784 | 3 | # | ||
785 | 4 | # Copyright 2013 Canonical Ltd. | ||
786 | 5 | # Written by: | ||
787 | 6 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
788 | 7 | # | ||
789 | 8 | # Checkbox is free software: you can redistribute it and/or modify | ||
790 | 9 | # it under the terms of the GNU General Public License version 3, | ||
791 | 10 | # as published by the Free Software Foundation. | ||
792 | 11 | |||
793 | 12 | # | ||
794 | 13 | # Checkbox is distributed in the hope that it will be useful, | ||
795 | 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
796 | 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
797 | 16 | # GNU General Public License for more details. | ||
798 | 17 | # | ||
799 | 18 | # You should have received a copy of the GNU General Public License | ||
800 | 19 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
801 | 20 | |||
802 | 21 | """ | ||
803 | 22 | :mod:`checkbox_ng.commands.sru` -- sru sub-command | ||
804 | 23 | ================================================== | ||
805 | 24 | |||
806 | 25 | .. warning:: | ||
807 | 26 | |||
808 | 27 | THIS MODULE DOES NOT HAVE STABLE PUBLIC API | ||
809 | 28 | """ | ||
810 | 29 | import sys | ||
811 | 30 | |||
812 | 31 | from gettext import gettext as _ | ||
813 | 32 | from plainbox.impl.commands import PlainBoxCommand | ||
814 | 33 | from plainbox.impl.commands.inv_check_config import CheckConfigInvocation | ||
815 | 34 | from plainbox.impl.ingredients import CanonicalCommand | ||
816 | 35 | from plainbox.impl.secure.config import ValidationError, Unset | ||
817 | 36 | |||
818 | 37 | |||
819 | 38 | class sru(CanonicalCommand): | ||
820 | 39 | |||
821 | 40 | """ | ||
822 | 41 | Run stable release update (sru) tests. | ||
823 | 42 | |||
824 | 43 | Stable release updates are periodic fixes for nominated bugs that land in | ||
825 | 44 | existing supported Ubuntu releases. To ensure a certain level of quality | ||
826 | 45 | all SRU updates affecting hardware enablement are automatically tested | ||
827 | 46 | on a pool of certified machines. | ||
828 | 47 | """ | ||
829 | 48 | |||
830 | 49 | def __init__(self, config): | ||
831 | 50 | """Init method to store the config settings.""" | ||
832 | 51 | self.config = config | ||
833 | 52 | if not self.config.test_plan: | ||
834 | 53 | self.config.test_plan = "2013.com.canonical.certification::sru" | ||
835 | 54 | |||
836 | 55 | def register_arguments(self, parser): | ||
837 | 56 | """Method called to register command line arguments.""" | ||
838 | 57 | parser.add_argument( | ||
839 | 58 | '--secure_id', metavar=_("SECURE-ID"), | ||
840 | 59 | # NOTE: --secure-id is optional only when set in a config file | ||
841 | 60 | required=self.config.secure_id is Unset, | ||
842 | 61 | help=_("Canonical hardware identifier")) | ||
843 | 62 | parser.add_argument( | ||
844 | 63 | '-T', '--test-plan', | ||
845 | 64 | action="store", | ||
846 | 65 | metavar=_("TEST-PLAN-ID"), | ||
847 | 66 | default=None, | ||
848 | 67 | # TRANSLATORS: this is in imperative form | ||
849 | 68 | help=_("load the specified test plan")) | ||
850 | 69 | parser.add_argument( | ||
851 | 70 | '--staging', action='store_true', default=False, | ||
852 | 71 | help=_("Send the data to non-production test server")) | ||
853 | 72 | parser.add_argument( | ||
854 | 73 | "--check-config", | ||
855 | 74 | action="store_true", | ||
856 | 75 | help=_("run check-config before starting")) | ||
857 | 76 | |||
858 | 77 | def invoked(self, ctx): | ||
859 | 78 | """Method called when the command is invoked.""" | ||
860 | 79 | # Copy command-line arguments over configuration variables | ||
861 | 80 | try: | ||
862 | 81 | if ctx.args.secure_id: | ||
863 | 82 | self.config.secure_id = ctx.args.secure_id | ||
864 | 83 | if ctx.args.test_plan: | ||
865 | 84 | self.config.test_plan = ctx.args.test_plan | ||
866 | 85 | if ctx.args.staging: | ||
867 | 86 | self.config.staging = ctx.args.staging | ||
868 | 87 | except ValidationError as exc: | ||
869 | 88 | print(_("Configuration problems prevent running SRU tests")) | ||
870 | 89 | print(exc) | ||
871 | 90 | return 1 | ||
872 | 91 | ctx.sa.use_alternate_configuration(self.config) | ||
873 | 92 | # Run check-config, if requested | ||
874 | 93 | if ctx.args.check_config: | ||
875 | 94 | retval = CheckConfigInvocation(lambda: self.config).run() | ||
876 | 95 | if retval != 0: | ||
877 | 96 | return retval | ||
878 | 97 | self.transport = self._create_transport( | ||
879 | 98 | ctx.sa, self.config.secure_id, self.config.staging) | ||
880 | 99 | self.ctx = ctx | ||
881 | 100 | try: | ||
882 | 101 | self._collect_info(ctx.rc, ctx.sa) | ||
883 | 102 | self._save_results(ctx.rc, ctx.sa) | ||
884 | 103 | self._send_results( | ||
885 | 104 | ctx.rc, ctx.sa, self.config.secure_id, self.config.staging) | ||
886 | 105 | except KeyboardInterrupt: | ||
887 | 106 | return 1 | ||
888 | 107 | |||
889 | 108 | def _save_results(self, rc, sa): | ||
890 | 109 | rc.reset() | ||
891 | 110 | rc.padding = (1, 1, 0, 1) | ||
892 | 111 | path = sa.export_to_file( | ||
893 | 112 | "2013.com.canonical.plainbox::hexr", (), '/tmp') | ||
894 | 113 | rc.para(_("Results saved to {0}").format(path)) | ||
895 | 114 | |||
896 | 115 | def _send_results(self, rc, sa, secure_id, staging): | ||
897 | 116 | rc.reset() | ||
898 | 117 | rc.padding = (1, 1, 0, 1) | ||
899 | 118 | rc.para(_("Sending hardware report to Canonical Certification")) | ||
900 | 119 | rc.para(_("Server URL is: {0}").format(self.transport.url)) | ||
901 | 120 | result = sa.export_to_transport( | ||
902 | 121 | "2013.com.canonical.plainbox::hexr", self.transport) | ||
903 | 122 | if 'url' in result: | ||
904 | 123 | rc.para(result['url']) | ||
905 | 124 | |||
906 | 125 | def _create_transport(self, sa, secure_id, staging): | ||
907 | 126 | return sa.get_canonical_certification_transport( | ||
908 | 127 | secure_id, staging=staging) | ||
909 | 128 | |||
910 | 129 | def _collect_info(self, rc, sa): | ||
911 | 130 | sa.select_providers('*') | ||
912 | 131 | sa.start_new_session(_("SRU Session")) | ||
913 | 132 | sa.select_test_plan(self.config.test_plan) | ||
914 | 133 | sa.bootstrap() | ||
915 | 134 | for job_id in sa.get_static_todo_list(): | ||
916 | 135 | job = sa.get_job(job_id) | ||
917 | 136 | builder = sa.run_job(job_id, 'silent', False) | ||
918 | 137 | result = builder.get_result() | ||
919 | 138 | sa.use_job_result(job_id, result) | ||
920 | 139 | rc.para("- {0}: {1}".format(job.id, result)) | ||
921 | 140 | if result.comments: | ||
922 | 141 | rc.padding = (0, 0, 0, 2) | ||
923 | 142 | rc.para("{0}".format(result.comments)) | ||
924 | 143 | rc.reset() | ||
925 | 144 | |||
926 | 145 | |||
927 | 146 | class SRUCommand(PlainBoxCommand): | ||
928 | 147 | |||
929 | 148 | """ | ||
930 | 149 | Command for running Stable Release Update (SRU) tests. | ||
931 | 150 | |||
932 | 151 | Stable release updates are periodic fixes for nominated bugs that land in | ||
933 | 152 | existing supported Ubuntu releases. To ensure a certain level of quality | ||
934 | 153 | all SRU updates affecting hardware enablement are automatically tested | ||
935 | 154 | on a pool of certified machines. | ||
936 | 155 | """ | ||
937 | 156 | |||
938 | 157 | gettext_domain = "checkbox-ng" | ||
939 | 158 | |||
940 | 159 | def __init__(self, provider_loader, config_loader): | ||
941 | 160 | self.provider_loader = provider_loader | ||
942 | 161 | # This command does funky things to the command line parser and it | ||
943 | 162 | # needs to load the config subsystem *early* so let's just load it now. | ||
944 | 163 | self.config = config_loader() | ||
945 | 164 | |||
946 | 165 | def invoked(self, ns): | ||
947 | 166 | """Method called when the command is invoked.""" | ||
948 | 167 | return sru(self.config).main(sys.argv[2:], exit=False) | ||
949 | 168 | |||
950 | 169 | def register_parser(self, subparsers): | ||
951 | 170 | """Method called to register command line arguments.""" | ||
952 | 171 | parser = subparsers.add_parser( | ||
953 | 172 | "sru", help=_("run automated stable release update tests")) | ||
954 | 173 | parser.set_defaults(command=self) | ||
955 | 174 | sru(self.config).register_arguments(parser) | ||
956 | diff --git a/checkbox_ng/commands/submit.py b/checkbox_ng/commands/submit.py | |||
957 | 175 | deleted file mode 100644 | 0 | deleted file mode 100644 |
958 | index 6d24a8a..0000000 | |||
959 | --- a/checkbox_ng/commands/submit.py | |||
960 | +++ /dev/null | |||
961 | @@ -1,154 +0,0 @@ | |||
962 | 1 | # This file is part of Checkbox. | ||
963 | 2 | # | ||
964 | 3 | # Copyright 2014 Canonical Ltd. | ||
965 | 4 | # Written by: | ||
966 | 5 | # Sylvain Pineau <sylvain.pineau@canonical.com> | ||
967 | 6 | # | ||
968 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
969 | 8 | # it under the terms of the GNU General Public License version 3, | ||
970 | 9 | # as published by the Free Software Foundation. | ||
971 | 10 | # | ||
972 | 11 | # Checkbox is distributed in the hope that it will be useful, | ||
973 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
974 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
975 | 14 | # GNU General Public License for more details. | ||
976 | 15 | # | ||
977 | 16 | # You should have received a copy of the GNU General Public License | ||
978 | 17 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
979 | 18 | |||
980 | 19 | """ | ||
981 | 20 | :mod:`checkbox_ng.commands.submit` -- the submit sub-command | ||
982 | 21 | ============================================================ | ||
983 | 22 | |||
984 | 23 | .. warning:: | ||
985 | 24 | |||
986 | 25 | THIS MODULE DOES NOT HAVE STABLE PUBLIC API | ||
987 | 26 | """ | ||
988 | 27 | |||
989 | 28 | from argparse import ArgumentTypeError | ||
990 | 29 | from plainbox.i18n import docstring | ||
991 | 30 | from plainbox.i18n import gettext as _ | ||
992 | 31 | from plainbox.i18n import gettext_noop as N_ | ||
993 | 32 | import re | ||
994 | 33 | |||
995 | 34 | from plainbox.impl.commands import PlainBoxCommand | ||
996 | 35 | from plainbox.impl.secure.config import Unset | ||
997 | 36 | from plainbox.impl.transport import TransportError | ||
998 | 37 | from plainbox.impl.transport import SECURE_ID_PATTERN | ||
999 | 38 | |||
1000 | 39 | from checkbox_ng.certification import CertificationTransport | ||
1001 | 40 | |||
1002 | 41 | |||
1003 | 42 | class SubmitInvocation: | ||
1004 | 43 | """ | ||
1005 | 44 | Helper class instantiated to perform a particular invocation of the submit | ||
1006 | 45 | command. Unlike the SRU command itself, this class is instantiated each | ||
1007 | 46 | time. | ||
1008 | 47 | """ | ||
1009 | 48 | |||
1010 | 49 | def __init__(self, ns): | ||
1011 | 50 | self.ns = ns | ||
1012 | 51 | |||
1013 | 52 | def run(self): | ||
1014 | 53 | options_string = "secure_id={0}".format(self.ns.secure_id) | ||
1015 | 54 | transport = CertificationTransport(self.ns.url, options_string) | ||
1016 | 55 | |||
1017 | 56 | try: | ||
1018 | 57 | with open(self.ns.submission, "r", encoding='utf-8') as subm_file: | ||
1019 | 58 | result = transport.send(subm_file) | ||
1020 | 59 | except (TransportError, OSError) as exc: | ||
1021 | 60 | raise SystemExit(exc) | ||
1022 | 61 | else: | ||
1023 | 62 | if 'url' in result: | ||
1024 | 63 | # TRANSLATORS: Do not translate the {} format marker. | ||
1025 | 64 | print(_("Successfully sent, submission status" | ||
1026 | 65 | " at {0}").format(result['url'])) | ||
1027 | 66 | else: | ||
1028 | 67 | # TRANSLATORS: Do not translate the {} format marker. | ||
1029 | 68 | print(_("Successfully sent, server response" | ||
1030 | 69 | ": {0}").format(result)) | ||
1031 | 70 | |||
1032 | 71 | |||
1033 | 72 | @docstring( | ||
1034 | 73 | # TRANSLATORS: please leave various options (both long and short forms), | ||
1035 | 74 | # environment variables and paths in their original form. Also keep the | ||
1036 | 75 | # special @EPILOG@ string. The first line of the translation is special and | ||
1037 | 76 | # is used as the help message. Please keep the pseudo-statement form and | ||
1038 | 77 | # don't finish the sentence with a dot. Pay extra attention to whitespace. | ||
1039 | 78 | # It must be correctly preserved or the result won't work. In particular | ||
1040 | 79 | # the leading whitespace *must* be preserved and *must* have the same | ||
1041 | 80 | # length on each line. | ||
1042 | 81 | N_(""" | ||
1043 | 82 | submit test results to the Canonical certification website | ||
1044 | 83 | |||
1045 | 84 | This command sends the XML results file to the Certification website. | ||
1046 | 85 | """)) | ||
1047 | 86 | class SubmitCommand(PlainBoxCommand): | ||
1048 | 87 | |||
1049 | 88 | gettext_domain = "checkbox-ng" | ||
1050 | 89 | |||
1051 | 90 | def __init__(self, config_loader): | ||
1052 | 91 | self.config = config_loader() | ||
1053 | 92 | |||
1054 | 93 | def invoked(self, ns): | ||
1055 | 94 | return SubmitInvocation(ns).run() | ||
1056 | 95 | |||
1057 | 96 | def register_parser(self, subparsers): | ||
1058 | 97 | parser = subparsers.add_parser("submit", help=_( | ||
1059 | 98 | "submit test results to the Canonical certification website")) | ||
1060 | 99 | self.register_arguments(parser) | ||
1061 | 100 | |||
1062 | 101 | def register_arguments(self, parser): | ||
1063 | 102 | parser.set_defaults(command=self) | ||
1064 | 103 | parser.add_argument( | ||
1065 | 104 | 'submission', help=_("The path to the results xml file")) | ||
1066 | 105 | self.register_optional_arguments(parser, required=True) | ||
1067 | 106 | |||
1068 | 107 | def register_optional_arguments(self, parser, required=False): | ||
1069 | 108 | if self.config.secure_id is not Unset: | ||
1070 | 109 | parser.set_defaults(secure_id=self.config.secure_id) | ||
1071 | 110 | |||
1072 | 111 | def secureid(secure_id): | ||
1073 | 112 | if not re.match(SECURE_ID_PATTERN, secure_id): | ||
1074 | 113 | raise ArgumentTypeError( | ||
1075 | 114 | _("must be 15-character (or more) alphanumeric string")) | ||
1076 | 115 | return secure_id | ||
1077 | 116 | |||
1078 | 117 | required_check = False | ||
1079 | 118 | if required: | ||
1080 | 119 | required_check = self.config.secure_id is Unset | ||
1081 | 120 | parser.add_argument( | ||
1082 | 121 | '--secure_id', metavar=_("SECURE-ID"), | ||
1083 | 122 | required=required_check, | ||
1084 | 123 | type=secureid, | ||
1085 | 124 | help=_("associate submission with a machine using this SECURE-ID")) | ||
1086 | 125 | |||
1087 | 126 | # Interpret this setting here | ||
1088 | 127 | # Please remember the Unset.__bool__() return False | ||
1089 | 128 | # After Interpret the setting, | ||
1090 | 129 | # self.config.submit_to_c3 should has value or be Unset. | ||
1091 | 130 | try: | ||
1092 | 131 | if (self.config.submit_to_c3 and | ||
1093 | 132 | (self.config.submit_to_c3.lower() in ('yes', 'true') or | ||
1094 | 133 | int(self.config.submit_to_c3) == 1)): | ||
1095 | 134 | # self.config.c3_url has a default value written in config.py | ||
1096 | 135 | parser.set_defaults(url=self.config.c3_url) | ||
1097 | 136 | else: | ||
1098 | 137 | # if submit_to_c3 is castable to int but not 1 | ||
1099 | 138 | # this is still set as Unset | ||
1100 | 139 | # otherwise url requirement will be None | ||
1101 | 140 | self.config.submit_to_c3 = Unset | ||
1102 | 141 | except ValueError: | ||
1103 | 142 | # When submit_to_c3 is something other than 'yes', 'true', | ||
1104 | 143 | # castable to integer, it raises ValueError. | ||
1105 | 144 | # e.g. 'no', 'false', 'asdf' ...etc. | ||
1106 | 145 | # In this case, it is still set as Unset. | ||
1107 | 146 | self.config.submit_to_c3 = Unset | ||
1108 | 147 | |||
1109 | 148 | required_check = False | ||
1110 | 149 | if required: | ||
1111 | 150 | required_check = self.config.submit_to_c3 is Unset | ||
1112 | 151 | parser.add_argument( | ||
1113 | 152 | '--url', metavar=_("URL"), | ||
1114 | 153 | required=required_check, | ||
1115 | 154 | help=_("destination to submit to")) | ||
1116 | diff --git a/checkbox_ng/commands/test_sru.py b/checkbox_ng/commands/test_sru.py | |||
1117 | 155 | deleted file mode 100644 | 0 | deleted file mode 100644 |
1118 | index 8d456ee..0000000 | |||
1119 | --- a/checkbox_ng/commands/test_sru.py | |||
1120 | +++ /dev/null | |||
1121 | @@ -1,56 +0,0 @@ | |||
1122 | 1 | # This file is part of Checkbox. | ||
1123 | 2 | # | ||
1124 | 3 | # Copyright 2013 Canonical Ltd. | ||
1125 | 4 | # Written by: | ||
1126 | 5 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
1127 | 6 | # | ||
1128 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
1129 | 8 | # it under the terms of the GNU General Public License version 3, | ||
1130 | 9 | # as published by the Free Software Foundation. | ||
1131 | 10 | |||
1132 | 11 | # | ||
1133 | 12 | # Checkbox is distributed in the hope that it will be useful, | ||
1134 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1135 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1136 | 15 | # GNU General Public License for more details. | ||
1137 | 16 | # | ||
1138 | 17 | # You should have received a copy of the GNU General Public License | ||
1139 | 18 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
1140 | 19 | |||
1141 | 20 | """ | ||
1142 | 21 | plainbox.impl.commands.test_sru | ||
1143 | 22 | =============================== | ||
1144 | 23 | |||
1145 | 24 | Test definitions for plainbox.impl.box module | ||
1146 | 25 | """ | ||
1147 | 26 | |||
1148 | 27 | from inspect import cleandoc | ||
1149 | 28 | from unittest import TestCase | ||
1150 | 29 | |||
1151 | 30 | from plainbox.testing_utils.io import TestIO | ||
1152 | 31 | |||
1153 | 32 | from checkbox_ng.main import main | ||
1154 | 33 | |||
1155 | 34 | |||
1156 | 35 | class TestSru(TestCase): | ||
1157 | 36 | |||
1158 | 37 | def test_help(self): | ||
1159 | 38 | with TestIO(combined=True) as io: | ||
1160 | 39 | with self.assertRaises(SystemExit) as call: | ||
1161 | 40 | main(['sru', '--help']) | ||
1162 | 41 | self.assertEqual(call.exception.args, (0,)) | ||
1163 | 42 | self.maxDiff = None | ||
1164 | 43 | expected = """ | ||
1165 | 44 | usage: checkbox sru [-h] --secure_id SECURE-ID [-T TEST-PLAN-ID] [--staging] | ||
1166 | 45 | [--check-config] | ||
1167 | 46 | |||
1168 | 47 | optional arguments: | ||
1169 | 48 | -h, --help show this help message and exit | ||
1170 | 49 | --secure_id SECURE-ID | ||
1171 | 50 | Canonical hardware identifier | ||
1172 | 51 | -T TEST-PLAN-ID, --test-plan TEST-PLAN-ID | ||
1173 | 52 | load the specified test plan | ||
1174 | 53 | --staging Send the data to non-production test server | ||
1175 | 54 | --check-config run check-config before starting | ||
1176 | 55 | """ | ||
1177 | 56 | self.assertEqual(io.combined, cleandoc(expected) + "\n") | ||
1178 | diff --git a/checkbox_ng/config.py b/checkbox_ng/config.py | |||
1179 | index 7107dc0..c84f2b8 100644 | |||
1180 | --- a/checkbox_ng/config.py | |||
1181 | +++ b/checkbox_ng/config.py | |||
1182 | @@ -22,13 +22,10 @@ | |||
1183 | 22 | ===================================================== | 22 | ===================================================== |
1184 | 23 | """ | 23 | """ |
1185 | 24 | 24 | ||
1186 | 25 | from gettext import gettext as _ | ||
1187 | 26 | import itertools | 25 | import itertools |
1188 | 27 | import os | 26 | import os |
1189 | 28 | 27 | ||
1190 | 29 | from plainbox.impl.applogic import PlainBoxConfig | 28 | from plainbox.impl.applogic import PlainBoxConfig |
1191 | 30 | from plainbox.impl.secure import config | ||
1192 | 31 | from plainbox.impl.transport import SECURE_ID_PATTERN | ||
1193 | 32 | 29 | ||
1194 | 33 | 30 | ||
1195 | 34 | class CheckBoxConfig(PlainBoxConfig): | 31 | class CheckBoxConfig(PlainBoxConfig): |
1196 | @@ -36,44 +33,6 @@ class CheckBoxConfig(PlainBoxConfig): | |||
1197 | 36 | Configuration for checkbox-ng | 33 | Configuration for checkbox-ng |
1198 | 37 | """ | 34 | """ |
1199 | 38 | 35 | ||
1200 | 39 | secure_id = config.Variable( | ||
1201 | 40 | section="sru", | ||
1202 | 41 | help_text=_("Secure ID of the system"), | ||
1203 | 42 | validator_list=[config.PatternValidator(SECURE_ID_PATTERN)]) | ||
1204 | 43 | |||
1205 | 44 | submit_to_c3 = config.Variable( | ||
1206 | 45 | section="submission", | ||
1207 | 46 | help_text=_("Whether to send the submission data to c3")) | ||
1208 | 47 | |||
1209 | 48 | submit_to_hexr = config.Variable( | ||
1210 | 49 | section="submission", | ||
1211 | 50 | help_text=_("Whether to also send the submission data to HEXR"), | ||
1212 | 51 | kind=bool) | ||
1213 | 52 | |||
1214 | 53 | # TODO: Add a validator to check if URL looks fine | ||
1215 | 54 | c3_url = config.Variable( | ||
1216 | 55 | section="sru", | ||
1217 | 56 | help_text=_("URL of the certification website"), | ||
1218 | 57 | default="https://certification.canonical.com/submissions/submit/") | ||
1219 | 58 | |||
1220 | 59 | fallback_file = config.Variable( | ||
1221 | 60 | section="sru", | ||
1222 | 61 | help_text=_("Location of the fallback file")) | ||
1223 | 62 | |||
1224 | 63 | whitelist = config.Variable( | ||
1225 | 64 | section="sru", | ||
1226 | 65 | help_text=_("Optional whitelist with which to run SRU testing")) | ||
1227 | 66 | |||
1228 | 67 | test_plan = config.Variable( | ||
1229 | 68 | section="sru", | ||
1230 | 69 | help_text=_("Optional test plan with which to run SRU testing")) | ||
1231 | 70 | |||
1232 | 71 | staging = config.Variable( | ||
1233 | 72 | section="sru", | ||
1234 | 73 | kind=bool, | ||
1235 | 74 | default=False, | ||
1236 | 75 | help_text=_("Send the data to non-production test server")) | ||
1237 | 76 | |||
1238 | 77 | class Meta(PlainBoxConfig.Meta): | 36 | class Meta(PlainBoxConfig.Meta): |
1239 | 78 | # TODO: properly depend on xdg and use real code that also handles | 37 | # TODO: properly depend on xdg and use real code that also handles |
1240 | 79 | # XDG_CONFIG_HOME. | 38 | # XDG_CONFIG_HOME. |
1241 | diff --git a/checkbox_ng/launcher/checkbox_cli.py b/checkbox_ng/launcher/checkbox_cli.py | |||
1242 | index 1317ed2..7c368c7 100644 | |||
1243 | --- a/checkbox_ng/launcher/checkbox_cli.py | |||
1244 | +++ b/checkbox_ng/launcher/checkbox_cli.py | |||
1245 | @@ -38,16 +38,15 @@ from plainbox.impl.ingredients import RenderingContextIngredient | |||
1246 | 38 | from plainbox.impl.ingredients import SessionAssistantIngredient | 38 | from plainbox.impl.ingredients import SessionAssistantIngredient |
1247 | 39 | from plainbox.impl.launcher import DefaultLauncherDefinition | 39 | from plainbox.impl.launcher import DefaultLauncherDefinition |
1248 | 40 | from plainbox.impl.launcher import LauncherDefinition | 40 | from plainbox.impl.launcher import LauncherDefinition |
1249 | 41 | from plainbox.vendor.textland import get_display | ||
1250 | 42 | 41 | ||
1251 | 43 | from checkbox_ng.launcher.subcommands import ( | 42 | from checkbox_ng.launcher.subcommands import ( |
1253 | 44 | Launcher, List, Run, StartProvider, ListBootstrapped | 43 | CheckConfig, Launcher, List, Run, StartProvider, Submit, ListBootstrapped |
1254 | 45 | ) | 44 | ) |
1255 | 46 | 45 | ||
1256 | 47 | 46 | ||
1257 | 48 | _ = gettext.gettext | 47 | _ = gettext.gettext |
1258 | 49 | 48 | ||
1260 | 50 | _logger = logging.getLogger("checkbox-launcher") | 49 | _logger = logging.getLogger("checkbox-cli") |
1261 | 51 | 50 | ||
1262 | 52 | 51 | ||
1263 | 53 | class DisplayIngredient(Ingredient): | 52 | class DisplayIngredient(Ingredient): |
1264 | @@ -125,7 +124,6 @@ class CheckboxCommandRecipe(CommandRecipe): | |||
1265 | 125 | LauncherIngredient(), | 124 | LauncherIngredient(), |
1266 | 126 | SessionAssistantIngredient(), | 125 | SessionAssistantIngredient(), |
1267 | 127 | RenderingContextIngredient(), | 126 | RenderingContextIngredient(), |
1268 | 128 | DisplayIngredient(), | ||
1269 | 129 | ] | 127 | ] |
1270 | 130 | 128 | ||
1271 | 131 | 129 | ||
1272 | @@ -141,10 +139,12 @@ class CheckboxCommand(CanonicalCommand): | |||
1273 | 141 | bug_report_url = "https://bugs.launchpad.net/checkbox-ng/+filebug" | 139 | bug_report_url = "https://bugs.launchpad.net/checkbox-ng/+filebug" |
1274 | 142 | 140 | ||
1275 | 143 | sub_commands = ( | 141 | sub_commands = ( |
1276 | 142 | ('check-config', CheckConfig), | ||
1277 | 144 | ('launcher', Launcher), | 143 | ('launcher', Launcher), |
1278 | 145 | ('list', List), | 144 | ('list', List), |
1279 | 146 | ('run', Run), | 145 | ('run', Run), |
1280 | 147 | ('startprovider', StartProvider), | 146 | ('startprovider', StartProvider), |
1281 | 147 | ('submit', Submit), | ||
1282 | 148 | ('list-bootstrapped', ListBootstrapped), | 148 | ('list-bootstrapped', ListBootstrapped), |
1283 | 149 | ) | 149 | ) |
1284 | 150 | 150 | ||
1285 | @@ -179,6 +179,7 @@ def main(): | |||
1286 | 179 | # $ checkbox-cli launcher my-launcher -> same as ^ | 179 | # $ checkbox-cli launcher my-launcher -> same as ^ |
1287 | 180 | # to achieve that the following code 'injects launcher subcommand to argv | 180 | # to achieve that the following code 'injects launcher subcommand to argv |
1288 | 181 | known_cmds = [x[0] for x in CheckboxCommand.sub_commands] | 181 | known_cmds = [x[0] for x in CheckboxCommand.sub_commands] |
1289 | 182 | known_cmds += ['-h', '--help'] | ||
1290 | 182 | if not (set(known_cmds) & set(sys.argv[1:])): | 183 | if not (set(known_cmds) & set(sys.argv[1:])): |
1291 | 183 | sys.argv.insert(1, 'launcher') | 184 | sys.argv.insert(1, 'launcher') |
1292 | 184 | CheckboxCommand().main() | 185 | CheckboxCommand().main() |
1293 | diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py | |||
1294 | index 7f52cda..155824a 100644 | |||
1295 | --- a/checkbox_ng/launcher/subcommands.py | |||
1296 | +++ b/checkbox_ng/launcher/subcommands.py | |||
1297 | @@ -37,6 +37,7 @@ from guacamole import Command | |||
1298 | 37 | from plainbox.abc import IJobResult | 37 | from plainbox.abc import IJobResult |
1299 | 38 | from plainbox.i18n import ngettext | 38 | from plainbox.i18n import ngettext |
1300 | 39 | from plainbox.impl.color import Colorizer | 39 | from plainbox.impl.color import Colorizer |
1301 | 40 | from plainbox.impl.commands.inv_check_config import CheckConfigInvocation | ||
1302 | 40 | from plainbox.impl.commands.inv_run import Action | 41 | from plainbox.impl.commands.inv_run import Action |
1303 | 41 | from plainbox.impl.commands.inv_run import NormalUI | 42 | from plainbox.impl.commands.inv_run import NormalUI |
1304 | 42 | from plainbox.impl.commands.inv_startprovider import ( | 43 | from plainbox.impl.commands.inv_startprovider import ( |
1305 | @@ -55,8 +56,10 @@ from plainbox.impl.session.restart import get_strategy_by_name | |||
1306 | 55 | from plainbox.impl.transport import TransportError | 56 | from plainbox.impl.transport import TransportError |
1307 | 56 | from plainbox.impl.transport import InvalidSecureIDError | 57 | from plainbox.impl.transport import InvalidSecureIDError |
1308 | 57 | from plainbox.impl.transport import get_all_transports | 58 | from plainbox.impl.transport import get_all_transports |
1309 | 59 | from plainbox.impl.transport import SECURE_ID_PATTERN | ||
1310 | 58 | from plainbox.public import get_providers | 60 | from plainbox.public import get_providers |
1311 | 59 | 61 | ||
1312 | 62 | from checkbox_ng.config import CheckBoxConfig | ||
1313 | 60 | from checkbox_ng.launcher.stages import MainLoopStage | 63 | from checkbox_ng.launcher.stages import MainLoopStage |
1314 | 61 | from checkbox_ng.urwid_ui import CategoryBrowser | 64 | from checkbox_ng.urwid_ui import CategoryBrowser |
1315 | 62 | from checkbox_ng.urwid_ui import ReRunBrowser | 65 | from checkbox_ng.urwid_ui import ReRunBrowser |
1316 | @@ -67,6 +70,73 @@ _ = gettext.gettext | |||
1317 | 67 | _logger = logging.getLogger("checkbox-ng.launcher.subcommands") | 70 | _logger = logging.getLogger("checkbox-ng.launcher.subcommands") |
1318 | 68 | 71 | ||
1319 | 69 | 72 | ||
1320 | 73 | class CheckConfig(Command): | ||
1321 | 74 | def invoked(self, ctx): | ||
1322 | 75 | return CheckConfigInvocation(lambda: CheckBoxConfig.get()).run() | ||
1323 | 76 | |||
1324 | 77 | |||
1325 | 78 | class Submit(Command): | ||
1326 | 79 | def register_arguments(self, parser): | ||
1327 | 80 | def secureid(secure_id): | ||
1328 | 81 | if not re.match(SECURE_ID_PATTERN, secure_id): | ||
1329 | 82 | raise ArgumentTypeError( | ||
1330 | 83 | _("must be 15-character (or more) alphanumeric string")) | ||
1331 | 84 | return secure_id | ||
1332 | 85 | parser.add_argument( | ||
1333 | 86 | 'secure_id', metavar=_("SECURE-ID"), | ||
1334 | 87 | type=secureid, | ||
1335 | 88 | help=_("associate submission with a machine using this SECURE-ID")) | ||
1336 | 89 | parser.add_argument( | ||
1337 | 90 | "submission", metavar=_("SUBMISSION"), | ||
1338 | 91 | help=_("The path to the results file")) | ||
1339 | 92 | parser.add_argument( | ||
1340 | 93 | "-s", "--staging", action="store_true", | ||
1341 | 94 | help=_("Use staging environment")) | ||
1342 | 95 | |||
1343 | 96 | def invoked(self, ctx): | ||
1344 | 97 | transport_cls = None | ||
1345 | 98 | enc = None | ||
1346 | 99 | mode = 'rb' | ||
1347 | 100 | options_string = "secure_id={0}".format(ctx.args.secure_id) | ||
1348 | 101 | url = ('https://submission.canonical.com/' | ||
1349 | 102 | 'v1/submission/hardware/{}'.format(ctx.args.secure_id)) | ||
1350 | 103 | if ctx.args.staging: | ||
1351 | 104 | url = ('https://submission.staging.canonical.com/' | ||
1352 | 105 | 'v1/submission/hardware/{}'.format(ctx.args.secure_id)) | ||
1353 | 106 | if ctx.args.submission.endswith('xml'): | ||
1354 | 107 | from checkbox_ng.certification import CertificationTransport | ||
1355 | 108 | transport_cls = CertificationTransport | ||
1356 | 109 | mode = 'r' | ||
1357 | 110 | enc = 'utf-8' | ||
1358 | 111 | url = ('https://certification.canonical.com/' | ||
1359 | 112 | 'submissions/submit/') | ||
1360 | 113 | if ctx.args.staging: | ||
1361 | 114 | url = ('https://certification.staging.canonical.com/' | ||
1362 | 115 | 'submissions/submit/') | ||
1363 | 116 | else: | ||
1364 | 117 | from checkbox_ng.certification import SubmissionServiceTransport | ||
1365 | 118 | transport_cls = SubmissionServiceTransport | ||
1366 | 119 | transport = transport_cls(url, options_string) | ||
1367 | 120 | try: | ||
1368 | 121 | with open(ctx.args.submission, mode, encoding=enc) as subm_file: | ||
1369 | 122 | result = transport.send(subm_file) | ||
1370 | 123 | except (TransportError, OSError) as exc: | ||
1371 | 124 | raise SystemExit(exc) | ||
1372 | 125 | else: | ||
1373 | 126 | if result and 'url' in result: | ||
1374 | 127 | # TRANSLATORS: Do not translate the {} format marker. | ||
1375 | 128 | print(_("Successfully sent, submission status" | ||
1376 | 129 | " at {0}").format(result['url'])) | ||
1377 | 130 | elif result and 'status_url' in result: | ||
1378 | 131 | # TRANSLATORS: Do not translate the {} format marker. | ||
1379 | 132 | print(_("Successfully sent, submission status" | ||
1380 | 133 | " at {0}").format(result['status_url'])) | ||
1381 | 134 | else: | ||
1382 | 135 | # TRANSLATORS: Do not translate the {} format marker. | ||
1383 | 136 | print(_("Successfully sent, server response" | ||
1384 | 137 | ": {0}").format(result)) | ||
1385 | 138 | |||
1386 | 139 | |||
1387 | 70 | class StartProvider(Command): | 140 | class StartProvider(Command): |
1388 | 71 | def register_arguments(self, parser): | 141 | def register_arguments(self, parser): |
1389 | 72 | parser.add_argument( | 142 | parser.add_argument( |
1390 | @@ -114,10 +184,6 @@ class Launcher(Command, MainLoopStage): | |||
1391 | 114 | print(_("Launcher seems valid.")) | 184 | print(_("Launcher seems valid.")) |
1392 | 115 | return | 185 | return |
1393 | 116 | self.launcher = ctx.cmd_toplevel.launcher | 186 | self.launcher = ctx.cmd_toplevel.launcher |
1394 | 117 | if not self.launcher.launcher_version: | ||
1395 | 118 | # it's a legacy launcher, use legacy way of running commands | ||
1396 | 119 | from checkbox_ng.tools import CheckboxLauncherTool | ||
1397 | 120 | raise SystemExit(CheckboxLauncherTool().main(sys.argv[1:])) | ||
1398 | 121 | logging_level = { | 187 | logging_level = { |
1399 | 122 | 'normal': logging.WARNING, | 188 | 'normal': logging.WARNING, |
1400 | 123 | 'verbose': logging.INFO, | 189 | 'verbose': logging.INFO, |
1401 | @@ -897,6 +963,7 @@ class List(Command): | |||
1402 | 897 | print(_("--format applies only to 'all-jobs' group. Ignoring...")) | 963 | print(_("--format applies only to 'all-jobs' group. Ignoring...")) |
1403 | 898 | print_objs(ctx.args.GROUP, ctx.args.attrs) | 964 | print_objs(ctx.args.GROUP, ctx.args.attrs) |
1404 | 899 | 965 | ||
1405 | 966 | |||
1406 | 900 | class ListBootstrapped(Command): | 967 | class ListBootstrapped(Command): |
1407 | 901 | name = 'list-bootstrapped' | 968 | name = 'list-bootstrapped' |
1408 | 902 | 969 | ||
1409 | diff --git a/checkbox_ng/main.py b/checkbox_ng/main.py | |||
1410 | 903 | deleted file mode 100644 | 970 | deleted file mode 100644 |
1411 | index 178edd9..0000000 | |||
1412 | --- a/checkbox_ng/main.py | |||
1413 | +++ /dev/null | |||
1414 | @@ -1,61 +0,0 @@ | |||
1415 | 1 | # This file is part of Checkbox. | ||
1416 | 2 | # | ||
1417 | 3 | # Copyright 2012-2014 Canonical Ltd. | ||
1418 | 4 | # Written by: | ||
1419 | 5 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
1420 | 6 | # | ||
1421 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
1422 | 8 | # it under the terms of the GNU General Public License version 3, | ||
1423 | 9 | # as published by the Free Software Foundation. | ||
1424 | 10 | # | ||
1425 | 11 | # Checkbox is distributed in the hope that it will be useful, | ||
1426 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1427 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1428 | 14 | # GNU General Public License for more details. | ||
1429 | 15 | # | ||
1430 | 16 | # You should have received a copy of the GNU General Public License | ||
1431 | 17 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
1432 | 18 | |||
1433 | 19 | """ | ||
1434 | 20 | :mod:`checkbox_ng.main` -- entry points for command line tools | ||
1435 | 21 | ============================================================== | ||
1436 | 22 | """ | ||
1437 | 23 | |||
1438 | 24 | import logging | ||
1439 | 25 | |||
1440 | 26 | from plainbox.impl.logging import setup_logging | ||
1441 | 27 | |||
1442 | 28 | from checkbox_ng.tools import CheckboxLauncherTool | ||
1443 | 29 | from checkbox_ng.tools import CheckboxSubmitTool | ||
1444 | 30 | from checkbox_ng.tools import CheckboxTool | ||
1445 | 31 | |||
1446 | 32 | |||
1447 | 33 | logger = logging.getLogger("checkbox.ng.main") | ||
1448 | 34 | |||
1449 | 35 | |||
1450 | 36 | def main(argv=None): | ||
1451 | 37 | """ | ||
1452 | 38 | checkbox command line utility | ||
1453 | 39 | """ | ||
1454 | 40 | raise SystemExit(CheckboxTool().main(argv)) | ||
1455 | 41 | |||
1456 | 42 | |||
1457 | 43 | def submit(argv=None): | ||
1458 | 44 | """ | ||
1459 | 45 | checkbox-submit command line utility | ||
1460 | 46 | """ | ||
1461 | 47 | raise SystemExit(CheckboxSubmitTool().main(argv)) | ||
1462 | 48 | |||
1463 | 49 | |||
1464 | 50 | def launcher(argv=None): | ||
1465 | 51 | """ | ||
1466 | 52 | checkbox-launcher command line utility | ||
1467 | 53 | """ | ||
1468 | 54 | raise SystemExit(CheckboxLauncherTool().main(argv)) | ||
1469 | 55 | |||
1470 | 56 | |||
1471 | 57 | # Setup logging before anything else starts working. | ||
1472 | 58 | # If we do it in main() or some other place then unit tests will see | ||
1473 | 59 | # "leaked" log files which are really closed when the runtime shuts | ||
1474 | 60 | # down but not when the tests are finishing | ||
1475 | 61 | setup_logging() | ||
1476 | diff --git a/checkbox_ng/misc.py b/checkbox_ng/misc.py | |||
1477 | 62 | deleted file mode 100644 | 0 | deleted file mode 100644 |
1478 | index e5547cd..0000000 | |||
1479 | --- a/checkbox_ng/misc.py | |||
1480 | +++ /dev/null | |||
1481 | @@ -1,476 +0,0 @@ | |||
1482 | 1 | # This file is part of Checkbox. | ||
1483 | 2 | # | ||
1484 | 3 | # Copyright 2013-2014 Canonical Ltd. | ||
1485 | 4 | # Written by: | ||
1486 | 5 | # Sylvain Pineau <sylvain.pineau@canonical.com> | ||
1487 | 6 | # | ||
1488 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
1489 | 8 | # it under the terms of the GNU General Public License version 3, | ||
1490 | 9 | # as published by the Free Software Foundation. | ||
1491 | 10 | # | ||
1492 | 11 | # Checkbox is distributed in the hope that it will be useful, | ||
1493 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1494 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1495 | 14 | # GNU General Public License for more details. | ||
1496 | 15 | # | ||
1497 | 16 | # You should have received a copy of the GNU General Public License | ||
1498 | 17 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
1499 | 18 | |||
1500 | 19 | """ | ||
1501 | 20 | :mod:`checkbox_ng.misc` -- Other stuff | ||
1502 | 21 | ====================================== | ||
1503 | 22 | |||
1504 | 23 | .. warning:: | ||
1505 | 24 | |||
1506 | 25 | THIS MODULE DOES NOT HAVE STABLE PUBLIC API | ||
1507 | 26 | """ | ||
1508 | 27 | |||
1509 | 28 | from gettext import gettext as _ | ||
1510 | 29 | from logging import getLogger | ||
1511 | 30 | |||
1512 | 31 | from plainbox.abc import IJobResult | ||
1513 | 32 | |||
1514 | 33 | |||
1515 | 34 | logger = getLogger("checkbox.ng.commands.cli") | ||
1516 | 35 | |||
1517 | 36 | |||
1518 | 37 | class JobTreeNode: | ||
1519 | 38 | |||
1520 | 39 | r""" | ||
1521 | 40 | JobTreeNode class is used to store a tree structure. | ||
1522 | 41 | |||
1523 | 42 | A tree consists of a collection of JobTreeNode instances connected in a | ||
1524 | 43 | hierarchical way where nodes are used as categories, jobs belonging to a | ||
1525 | 44 | category are listed in the node leaves. | ||
1526 | 45 | |||
1527 | 46 | Example:: | ||
1528 | 47 | / Job A | ||
1529 | 48 | Root-| | ||
1530 | 49 | | / Job B | ||
1531 | 50 | \--- Category X | | ||
1532 | 51 | \ Job C | ||
1533 | 52 | """ | ||
1534 | 53 | |||
1535 | 54 | def __init__(self, name=None): | ||
1536 | 55 | """ Initialize the job tree node with a given name. """ | ||
1537 | 56 | self._name = name if name else 'Root' | ||
1538 | 57 | self._parent = None | ||
1539 | 58 | self._categories = [] | ||
1540 | 59 | self._jobs = [] | ||
1541 | 60 | |||
1542 | 61 | @property | ||
1543 | 62 | def name(self): | ||
1544 | 63 | """ name of this node. """ | ||
1545 | 64 | return self._name | ||
1546 | 65 | |||
1547 | 66 | @property | ||
1548 | 67 | def parent(self): | ||
1549 | 68 | """ parent node for this node. """ | ||
1550 | 69 | return self._parent | ||
1551 | 70 | |||
1552 | 71 | @property | ||
1553 | 72 | def categories(self): | ||
1554 | 73 | """ list of sub categories. """ | ||
1555 | 74 | return self._categories | ||
1556 | 75 | |||
1557 | 76 | @property | ||
1558 | 77 | def jobs(self): | ||
1559 | 78 | """ job(s) belonging to this node/category. """ | ||
1560 | 79 | return self._jobs | ||
1561 | 80 | |||
1562 | 81 | @property | ||
1563 | 82 | def depth(self): | ||
1564 | 83 | """ level of depth for this node. """ | ||
1565 | 84 | return (self._parent.depth + 1) if self._parent else 0 | ||
1566 | 85 | |||
1567 | 86 | def __str__(self): | ||
1568 | 87 | """ same as self.name. """ | ||
1569 | 88 | return self.name | ||
1570 | 89 | |||
1571 | 90 | def __repr__(self): | ||
1572 | 91 | """ Get a representation of this node for debugging. """ | ||
1573 | 92 | return "<JobTreeNode name:{!r}>".format(self.name) | ||
1574 | 93 | |||
1575 | 94 | def add_category(self, category): | ||
1576 | 95 | """ | ||
1577 | 96 | Add a new category to this node. | ||
1578 | 97 | |||
1579 | 98 | :param category: | ||
1580 | 99 | The node instance to be added as a category. | ||
1581 | 100 | """ | ||
1582 | 101 | self._categories.append(category) | ||
1583 | 102 | # Always keep this list sorted to easily find a given child by index | ||
1584 | 103 | self._categories.sort(key=lambda item: item.name) | ||
1585 | 104 | category._parent = self | ||
1586 | 105 | |||
1587 | 106 | def add_job(self, job): | ||
1588 | 107 | """ | ||
1589 | 108 | Add a new job to this node. | ||
1590 | 109 | |||
1591 | 110 | :param job: | ||
1592 | 111 | The job instance to be added to this node. | ||
1593 | 112 | """ | ||
1594 | 113 | self._jobs.append(job) | ||
1595 | 114 | # Always keep this list sorted to easily find a given leaf by index | ||
1596 | 115 | # Note bisect.insort(a, x) cannot be used here as JobDefinition are | ||
1597 | 116 | # not sortable | ||
1598 | 117 | self._jobs.sort(key=lambda item: item.id) | ||
1599 | 118 | |||
1600 | 119 | def get_ancestors(self): | ||
1601 | 120 | """ Get the list of ancestors from here to the root of the tree. """ | ||
1602 | 121 | ancestors = [] | ||
1603 | 122 | node = self | ||
1604 | 123 | while node.parent is not None: | ||
1605 | 124 | ancestors.append(node.parent) | ||
1606 | 125 | node = node.parent | ||
1607 | 126 | return ancestors | ||
1608 | 127 | |||
1609 | 128 | def get_descendants(self): | ||
1610 | 129 | """ Return a list of all descendant category nodes. """ | ||
1611 | 130 | descendants = [] | ||
1612 | 131 | for category in self.categories: | ||
1613 | 132 | descendants.append(category) | ||
1614 | 133 | descendants.extend(category.get_descendants()) | ||
1615 | 134 | return descendants | ||
1616 | 135 | |||
1617 | 136 | @classmethod | ||
1618 | 137 | def create_tree(cls, session_state, job_list): | ||
1619 | 138 | """ | ||
1620 | 139 | Build a rooted JobTreeNode from a job list. | ||
1621 | 140 | |||
1622 | 141 | :argument session_state: | ||
1623 | 142 | A session state object | ||
1624 | 143 | :argument job_list: | ||
1625 | 144 | List of jobs to consider for building the tree. | ||
1626 | 145 | """ | ||
1627 | 146 | builder = TreeBuilder(session_state, cls) | ||
1628 | 147 | for job in job_list: | ||
1629 | 148 | builder.auto_add_job(job) | ||
1630 | 149 | return builder.root_node | ||
1631 | 150 | |||
1632 | 151 | @classmethod | ||
1633 | 152 | def create_simple_tree(cls, sa, job_list): | ||
1634 | 153 | """ | ||
1635 | 154 | Build a rooted JobTreeNode from a job list. | ||
1636 | 155 | |||
1637 | 156 | :argument sa: | ||
1638 | 157 | A session assistant object | ||
1639 | 158 | :argument job_list: | ||
1640 | 159 | List of jobs to consider for building the tree. | ||
1641 | 160 | """ | ||
1642 | 161 | root_node = cls() | ||
1643 | 162 | for job in job_list: | ||
1644 | 163 | cat_id = sa.get_job_state(job.id).effective_category_id | ||
1645 | 164 | cat_name = sa.get_category(cat_id).tr_name() | ||
1646 | 165 | matches = [n for n in root_node.categories if n.name == cat_name] | ||
1647 | 166 | if not matches: | ||
1648 | 167 | node = cls(cat_name) | ||
1649 | 168 | root_node.add_category(node) | ||
1650 | 169 | else: | ||
1651 | 170 | node = matches[0] | ||
1652 | 171 | node.add_job(job) | ||
1653 | 172 | return root_node | ||
1654 | 173 | |||
1655 | 174 | @classmethod | ||
1656 | 175 | def create_rerun_tree(cls, sa, job_list): | ||
1657 | 176 | """ | ||
1658 | 177 | Build a rooted JobTreeNode from a job list for the re-run screen. | ||
1659 | 178 | The jobs are categorized by their outcome (failed, skipped, ...) | ||
1660 | 179 | instead of by the category they belong to. | ||
1661 | 180 | |||
1662 | 181 | :argument sa: | ||
1663 | 182 | A session assistant object | ||
1664 | 183 | :argument job_list: | ||
1665 | 184 | List of jobs to consider for building the tree. | ||
1666 | 185 | """ | ||
1667 | 186 | section_names = { | ||
1668 | 187 | IJobResult.OUTCOME_FAIL: _("Failed Jobs"), | ||
1669 | 188 | IJobResult.OUTCOME_SKIP: _("Skipped Jobs"), | ||
1670 | 189 | IJobResult.OUTCOME_CRASH: _("Crashed Jobs"), | ||
1671 | 190 | } | ||
1672 | 191 | root_node = cls() | ||
1673 | 192 | for job in job_list: | ||
1674 | 193 | cat_id = sa.get_job_state(job.id).effective_category_id | ||
1675 | 194 | cat_name = sa.get_category(cat_id).tr_name() | ||
1676 | 195 | job_outcome = sa.get_job_state(job.id).result.outcome | ||
1677 | 196 | job_section = section_names[job_outcome] | ||
1678 | 197 | matches = [n for n in root_node.categories if n.name == job_section] | ||
1679 | 198 | if not matches: | ||
1680 | 199 | node = cls(job_section) | ||
1681 | 200 | root_node.add_category(node) | ||
1682 | 201 | else: | ||
1683 | 202 | node = matches[0] | ||
1684 | 203 | |||
1685 | 204 | node.add_job(job) | ||
1686 | 205 | return root_node | ||
1687 | 206 | |||
1688 | 207 | |||
1689 | 208 | class TreeBuilder: | ||
1690 | 209 | |||
1691 | 210 | """ | ||
1692 | 211 | Builder for :class:`JobTreeNode`. | ||
1693 | 212 | |||
1694 | 213 | |||
1695 | 214 | Helper class that assists in building a tree of :class:`JobTreeNode` | ||
1696 | 215 | objects out of job definitions and their associations, as expressed by | ||
1697 | 216 | :attr:`JobState.via_job` associated with each job. | ||
1698 | 217 | |||
1699 | 218 | The builder is a single-use object and should be re-created for each new | ||
1700 | 219 | construct. Internally it stores the job_state_map of the | ||
1701 | 220 | :class:`SessionState` it was created with as well as additional helper | ||
1702 | 221 | state. | ||
1703 | 222 | """ | ||
1704 | 223 | |||
1705 | 224 | def __init__(self, session_state: "SessionState", node_cls): | ||
1706 | 225 | self._job_state_map = session_state.job_state_map | ||
1707 | 226 | self._node_cls = node_cls | ||
1708 | 227 | self._root_node = node_cls() | ||
1709 | 228 | self._category_node_map = {} # id -> node | ||
1710 | 229 | |||
1711 | 230 | @property | ||
1712 | 231 | def root_node(self): | ||
1713 | 232 | return self._root_node | ||
1714 | 233 | |||
1715 | 234 | def auto_add_job(self, job): | ||
1716 | 235 | """ | ||
1717 | 236 | Add a job to the tree, automatically creating category nodes as needed. | ||
1718 | 237 | |||
1719 | 238 | :param job: | ||
1720 | 239 | The job definition to add. | ||
1721 | 240 | """ | ||
1722 | 241 | if job.plugin == 'local': | ||
1723 | 242 | # For local jobs, just create the category node but don't add the | ||
1724 | 243 | # local job itself there. | ||
1725 | 244 | self.get_or_create_category_node(job) | ||
1726 | 245 | else: | ||
1727 | 246 | # For all other jobs, look at the parent job (if any) and create | ||
1728 | 247 | # the category node out of that node. This never fails as "None" is | ||
1729 | 248 | # the root_node object. | ||
1730 | 249 | state = self._job_state_map[job.id] | ||
1731 | 250 | node = self.get_or_create_category_node(state.via_job) | ||
1732 | 251 | # Then add that job to the category node | ||
1733 | 252 | node.add_job(job) | ||
1734 | 253 | |||
1735 | 254 | def get_or_create_category_node(self, category_job): | ||
1736 | 255 | """ | ||
1737 | 256 | Get a category node for a given job. | ||
1738 | 257 | |||
1739 | 258 | Get or create a :class:`JobTreeNode` that corresponds to the | ||
1740 | 259 | category defined (somehow) by the job ``category_job``. | ||
1741 | 260 | |||
1742 | 261 | :param category_job: | ||
1743 | 262 | The job that describes the category. This is either a | ||
1744 | 263 | plugin="local" job or a plugin="resource" job. This can also be | ||
1745 | 264 | None, which is a shorthand to say "root node". | ||
1746 | 265 | :returns: | ||
1747 | 266 | The ``root_node`` if ``category_job`` is None. A freshly | ||
1748 | 267 | created node, created with :func:`create_category_node()` if | ||
1749 | 268 | the category_job was never seen before (as recorded by the | ||
1750 | 269 | category_node_map). | ||
1751 | 270 | """ | ||
1752 | 271 | logger.debug("get_or_create_category_node(%r)", category_job) | ||
1753 | 272 | if category_job is None: | ||
1754 | 273 | return self._root_node | ||
1755 | 274 | if category_job.id not in self._category_node_map: | ||
1756 | 275 | category_node = self.create_category_node(category_job) | ||
1757 | 276 | # The category is added to its parent, that's either the root | ||
1758 | 277 | # (if we're standalone) or the non-root category this one | ||
1759 | 278 | # belongs to. | ||
1760 | 279 | category_state = self._job_state_map[category_job.id] | ||
1761 | 280 | if category_state.via_job is not None: | ||
1762 | 281 | parent_category_node = self.get_or_create_category_node( | ||
1763 | 282 | category_state.via_job) | ||
1764 | 283 | else: | ||
1765 | 284 | parent_category_node = self._root_node | ||
1766 | 285 | parent_category_node.add_category(category_node) | ||
1767 | 286 | else: | ||
1768 | 287 | category_node = self._category_node_map[category_job.id] | ||
1769 | 288 | return category_node | ||
1770 | 289 | |||
1771 | 290 | def create_category_node(self, category_job): | ||
1772 | 291 | """ | ||
1773 | 292 | Create a category node for a given job. | ||
1774 | 293 | |||
1775 | 294 | Create a :class:`JobTreeNode` that corresponds to the category defined | ||
1776 | 295 | (somehow) by the job ``category_job``. | ||
1777 | 296 | |||
1778 | 297 | :param category_job: | ||
1779 | 298 | The job that describes the node to create. | ||
1780 | 299 | :returns: | ||
1781 | 300 | A fresh node with appropriate data. | ||
1782 | 301 | """ | ||
1783 | 302 | logger.debug("create_category_node(%r)", category_job) | ||
1784 | 303 | if category_job.summary == category_job.partial_id: | ||
1785 | 304 | category_node = self._node_cls(category_job.description) | ||
1786 | 305 | else: | ||
1787 | 306 | category_node = self._node_cls(category_job.summary) | ||
1788 | 307 | self._category_node_map[category_job.id] = category_node | ||
1789 | 308 | return category_node | ||
1790 | 309 | |||
1791 | 310 | |||
1792 | 311 | class SelectableJobTreeNode(JobTreeNode): | ||
1793 | 312 | """ | ||
1794 | 313 | Implementation of a node in a tree that can be selected/deselected | ||
1795 | 314 | """ | ||
1796 | 315 | def __init__(self, job=None): | ||
1797 | 316 | super().__init__(job) | ||
1798 | 317 | self.selected = True | ||
1799 | 318 | self.job_selection = {} | ||
1800 | 319 | self.expanded = True | ||
1801 | 320 | self.current_index = 0 | ||
1802 | 321 | self._resource_jobs = [] | ||
1803 | 322 | |||
1804 | 323 | def __len__(self): | ||
1805 | 324 | l = 0 | ||
1806 | 325 | if self.expanded: | ||
1807 | 326 | for category in self.categories: | ||
1808 | 327 | l += 1 + len(category) | ||
1809 | 328 | for job in self.jobs: | ||
1810 | 329 | l += 1 | ||
1811 | 330 | return l | ||
1812 | 331 | |||
1813 | 332 | def get_node_by_index(self, index, tree=None): | ||
1814 | 333 | """ | ||
1815 | 334 | Return the node found at the position given by index considering the | ||
1816 | 335 | tree from a top-down list view. | ||
1817 | 336 | """ | ||
1818 | 337 | if tree is None: | ||
1819 | 338 | tree = self | ||
1820 | 339 | if self.expanded: | ||
1821 | 340 | for category in self.categories: | ||
1822 | 341 | if index == tree.current_index: | ||
1823 | 342 | tree.current_index = 0 | ||
1824 | 343 | return (category, None) | ||
1825 | 344 | else: | ||
1826 | 345 | tree.current_index += 1 | ||
1827 | 346 | result = category.get_node_by_index(index, tree) | ||
1828 | 347 | if result[0] is not None and result[1] is not None: | ||
1829 | 348 | return result | ||
1830 | 349 | for job in self.jobs: | ||
1831 | 350 | if index == tree.current_index: | ||
1832 | 351 | tree.current_index = 0 | ||
1833 | 352 | return (job, self) | ||
1834 | 353 | else: | ||
1835 | 354 | tree.current_index += 1 | ||
1836 | 355 | return (None, None) | ||
1837 | 356 | |||
1838 | 357 | def render(self, cols=80, as_summary=True): | ||
1839 | 358 | """ | ||
1840 | 359 | Return the tree as a simple list of categories and jobs suitable for | ||
1841 | 360 | display. Jobs are properly indented to respect the tree hierarchy | ||
1842 | 361 | and selection marks are added automatically at the beginning of each | ||
1843 | 362 | element. | ||
1844 | 363 | |||
1845 | 364 | The node titles should not exceed the width of a the terminal and | ||
1846 | 365 | thus are cut to fit inside. | ||
1847 | 366 | |||
1848 | 367 | :param cols: | ||
1849 | 368 | The number of columns to render. | ||
1850 | 369 | :param as_summary: | ||
1851 | 370 | Whether we display the job summaries or their partial IDs. | ||
1852 | 371 | """ | ||
1853 | 372 | self._flat_list = [] | ||
1854 | 373 | if self.expanded: | ||
1855 | 374 | for category in self.categories: | ||
1856 | 375 | prefix = '[ ]' | ||
1857 | 376 | if category.selected: | ||
1858 | 377 | prefix = '[X]' | ||
1859 | 378 | line = '' | ||
1860 | 379 | title = category.name | ||
1861 | 380 | if category.jobs or category.categories: | ||
1862 | 381 | if category.expanded: | ||
1863 | 382 | line = prefix + self.depth * ' ' + ' - ' + title | ||
1864 | 383 | else: | ||
1865 | 384 | line = prefix + self.depth * ' ' + ' + ' + title | ||
1866 | 385 | else: | ||
1867 | 386 | line = prefix + self.depth * ' ' + ' ' + title | ||
1868 | 387 | if len(line) > cols: | ||
1869 | 388 | col_max = cols - 4 # includes len('...') + a space | ||
1870 | 389 | line = line[:col_max] + '...' | ||
1871 | 390 | self._flat_list.append(line) | ||
1872 | 391 | self._flat_list.extend(category.render(cols, as_summary)) | ||
1873 | 392 | for job in self.jobs: | ||
1874 | 393 | prefix = '[ ]' | ||
1875 | 394 | if self.job_selection[job]: | ||
1876 | 395 | prefix = '[X]' | ||
1877 | 396 | if as_summary: | ||
1878 | 397 | title = job.tr_summary() | ||
1879 | 398 | else: | ||
1880 | 399 | title = job.partial_id | ||
1881 | 400 | line = prefix + self.depth * ' ' + ' ' + title | ||
1882 | 401 | if len(line) > cols: | ||
1883 | 402 | col_max = cols - 4 # includes len('...') + a space | ||
1884 | 403 | line = line[:col_max] + '...' | ||
1885 | 404 | self._flat_list.append(line) | ||
1886 | 405 | return self._flat_list | ||
1887 | 406 | |||
1888 | 407 | def add_job(self, job): | ||
1889 | 408 | if job.plugin == 'resource': | ||
1890 | 409 | # I don't want the user to see resources but I need to keep | ||
1891 | 410 | # track of them to put them in the final selection. I also | ||
1892 | 411 | # don't want to add them to the tree. | ||
1893 | 412 | self._resource_jobs.append(job) | ||
1894 | 413 | return | ||
1895 | 414 | super().add_job(job) | ||
1896 | 415 | self.job_selection[job] = True | ||
1897 | 416 | |||
1898 | 417 | @property | ||
1899 | 418 | def selection(self): | ||
1900 | 419 | """ | ||
1901 | 420 | Return all the jobs currently selected | ||
1902 | 421 | """ | ||
1903 | 422 | self._selection_list = [] | ||
1904 | 423 | for category in self.categories: | ||
1905 | 424 | self._selection_list.extend(category.selection) | ||
1906 | 425 | for job in self.job_selection: | ||
1907 | 426 | if self.job_selection[job]: | ||
1908 | 427 | self._selection_list.append(job) | ||
1909 | 428 | return self._selection_list | ||
1910 | 429 | |||
1911 | 430 | @property | ||
1912 | 431 | def resource_jobs(self): | ||
1913 | 432 | """Return all the resource jobs.""" | ||
1914 | 433 | return self._resource_jobs | ||
1915 | 434 | |||
1916 | 435 | def set_ancestors_state(self, new_state): | ||
1917 | 436 | """ | ||
1918 | 437 | Set the selection state of all ancestors consistently | ||
1919 | 438 | """ | ||
1920 | 439 | # If child is set, then all ancestors must be set | ||
1921 | 440 | if new_state: | ||
1922 | 441 | parent = self.parent | ||
1923 | 442 | while parent: | ||
1924 | 443 | parent.selected = new_state | ||
1925 | 444 | parent = parent.parent | ||
1926 | 445 | # If child is not set, then all ancestors mustn't be set | ||
1927 | 446 | # unless another child of the ancestor is set | ||
1928 | 447 | else: | ||
1929 | 448 | parent = self.parent | ||
1930 | 449 | while parent: | ||
1931 | 450 | if any((category.selected | ||
1932 | 451 | for category in parent.categories)): | ||
1933 | 452 | break | ||
1934 | 453 | if any((parent.job_selection[job] | ||
1935 | 454 | for job in parent.job_selection)): | ||
1936 | 455 | break | ||
1937 | 456 | parent.selected = new_state | ||
1938 | 457 | parent = parent.parent | ||
1939 | 458 | |||
1940 | 459 | def update_selected_state(self): | ||
1941 | 460 | """ | ||
1942 | 461 | Update the category state according to its job selection | ||
1943 | 462 | """ | ||
1944 | 463 | if any((self.job_selection[job] for job in self.job_selection)): | ||
1945 | 464 | self.selected = True | ||
1946 | 465 | else: | ||
1947 | 466 | self.selected = False | ||
1948 | 467 | |||
1949 | 468 | def set_descendants_state(self, new_state): | ||
1950 | 469 | """ | ||
1951 | 470 | Set the selection state of all descendants recursively | ||
1952 | 471 | """ | ||
1953 | 472 | self.selected = new_state | ||
1954 | 473 | for job in self.job_selection: | ||
1955 | 474 | self.job_selection[job] = new_state | ||
1956 | 475 | for category in self.categories: | ||
1957 | 476 | category.set_descendants_state(new_state) | ||
1958 | diff --git a/checkbox_ng/test_config.py b/checkbox_ng/test_config.py | |||
1959 | 477 | deleted file mode 100644 | 0 | deleted file mode 100644 |
1960 | index 33e3a24..0000000 | |||
1961 | --- a/checkbox_ng/test_config.py | |||
1962 | +++ /dev/null | |||
1963 | @@ -1,46 +0,0 @@ | |||
1964 | 1 | # This file is part of Checkbox. | ||
1965 | 2 | # | ||
1966 | 3 | # Copyright 2013 Canonical Ltd. | ||
1967 | 4 | # Written by: | ||
1968 | 5 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
1969 | 6 | # | ||
1970 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
1971 | 8 | # it under the terms of the GNU General Public License version 3, | ||
1972 | 9 | # as published by the Free Software Foundation. | ||
1973 | 10 | |||
1974 | 11 | # | ||
1975 | 12 | # Checkbox is distributed in the hope that it will be useful, | ||
1976 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1977 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1978 | 15 | # GNU General Public License for more details. | ||
1979 | 16 | # | ||
1980 | 17 | # You should have received a copy of the GNU General Public License | ||
1981 | 18 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
1982 | 19 | |||
1983 | 20 | """ | ||
1984 | 21 | checkbox_ng.test_config | ||
1985 | 22 | ======================= | ||
1986 | 23 | |||
1987 | 24 | Test definitions for checkbox_ng.config module | ||
1988 | 25 | """ | ||
1989 | 26 | |||
1990 | 27 | from unittest import TestCase | ||
1991 | 28 | |||
1992 | 29 | from plainbox.impl.secure.config import Unset | ||
1993 | 30 | |||
1994 | 31 | from checkbox_ng.config import CheckBoxConfig | ||
1995 | 32 | |||
1996 | 33 | |||
1997 | 34 | class PlainBoxConfigTests(TestCase): | ||
1998 | 35 | |||
1999 | 36 | def test_smoke(self): | ||
2000 | 37 | config = CheckBoxConfig() | ||
2001 | 38 | self.assertIs(config.secure_id, Unset) | ||
2002 | 39 | secure_id = "0123456789ABCDE" | ||
2003 | 40 | config.secure_id = secure_id | ||
2004 | 41 | self.assertEqual(config.secure_id, secure_id) | ||
2005 | 42 | with self.assertRaises(ValueError): | ||
2006 | 43 | config.secure_id = "bork" | ||
2007 | 44 | self.assertEqual(config.secure_id, secure_id) | ||
2008 | 45 | del config.secure_id | ||
2009 | 46 | self.assertIs(config.secure_id, Unset) | ||
2010 | diff --git a/checkbox_ng/test_main.py b/checkbox_ng/test_main.py | |||
2011 | 47 | deleted file mode 100644 | 0 | deleted file mode 100644 |
2012 | index 1d2981d..0000000 | |||
2013 | --- a/checkbox_ng/test_main.py | |||
2014 | +++ /dev/null | |||
2015 | @@ -1,93 +0,0 @@ | |||
2016 | 1 | # This file is part of Checkbox. | ||
2017 | 2 | # | ||
2018 | 3 | # Copyright 2012-2014 Canonical Ltd. | ||
2019 | 4 | # Written by: | ||
2020 | 5 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
2021 | 6 | # | ||
2022 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
2023 | 8 | # it under the terms of the GNU General Public License version 3, | ||
2024 | 9 | # as published by the Free Software Foundation. | ||
2025 | 10 | |||
2026 | 11 | # | ||
2027 | 12 | # Checkbox is distributed in the hope that it will be useful, | ||
2028 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2029 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2030 | 15 | # GNU General Public License for more details. | ||
2031 | 16 | # | ||
2032 | 17 | # You should have received a copy of the GNU General Public License | ||
2033 | 18 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
2034 | 19 | |||
2035 | 20 | """ | ||
2036 | 21 | checkbox_ng.test_main | ||
2037 | 22 | ===================== | ||
2038 | 23 | |||
2039 | 24 | Test definitions for checkbox_ng.main module | ||
2040 | 25 | """ | ||
2041 | 26 | |||
2042 | 27 | from inspect import cleandoc | ||
2043 | 28 | from unittest import TestCase | ||
2044 | 29 | |||
2045 | 30 | from plainbox.impl.clitools import ToolBase | ||
2046 | 31 | from plainbox.testing_utils.io import TestIO | ||
2047 | 32 | |||
2048 | 33 | from checkbox_ng import __version__ as version | ||
2049 | 34 | from checkbox_ng.main import main | ||
2050 | 35 | |||
2051 | 36 | |||
2052 | 37 | class TestMain(TestCase): | ||
2053 | 38 | |||
2054 | 39 | def test_version(self): | ||
2055 | 40 | with TestIO(combined=True) as io: | ||
2056 | 41 | with self.assertRaises(SystemExit) as call: | ||
2057 | 42 | main(['--version']) | ||
2058 | 43 | self.assertEqual(call.exception.args, (0,)) | ||
2059 | 44 | self.assertEqual(io.combined, "{}\n".format(version)) | ||
2060 | 45 | |||
2061 | 46 | def test_help(self): | ||
2062 | 47 | with TestIO(combined=True) as io: | ||
2063 | 48 | with self.assertRaises(SystemExit) as call: | ||
2064 | 49 | main(['--help']) | ||
2065 | 50 | self.assertEqual(call.exception.args, (0,)) | ||
2066 | 51 | self.maxDiff = None | ||
2067 | 52 | expected = """ | ||
2068 | 53 | usage: checkbox [-h] [--version] [-v] [-D] [-C] [-T LOGGER] [-P] [-I] | ||
2069 | 54 | {sru,check-config,submit,launcher,self-test} ... | ||
2070 | 55 | |||
2071 | 56 | positional arguments: | ||
2072 | 57 | {sru,check-config,submit,launcher,self-test} | ||
2073 | 58 | sru run automated stable release update tests | ||
2074 | 59 | check-config check and display plainbox configuration | ||
2075 | 60 | submit submit test results to the Canonical certification | ||
2076 | 61 | website | ||
2077 | 62 | launcher run a customized testing session | ||
2078 | 63 | self-test run unit and integration tests | ||
2079 | 64 | |||
2080 | 65 | optional arguments: | ||
2081 | 66 | -h, --help show this help message and exit | ||
2082 | 67 | --version show program's version number and exit | ||
2083 | 68 | |||
2084 | 69 | logging and debugging: | ||
2085 | 70 | -v, --verbose be more verbose (same as --log-level=INFO) | ||
2086 | 71 | -D, --debug enable DEBUG messages on the root logger | ||
2087 | 72 | -C, --debug-console display DEBUG messages in the console | ||
2088 | 73 | -T LOGGER, --trace LOGGER | ||
2089 | 74 | enable DEBUG messages on the specified logger (can be | ||
2090 | 75 | used multiple times) | ||
2091 | 76 | -P, --pdb jump into pdb (python debugger) when a command crashes | ||
2092 | 77 | -I, --debug-interrupt | ||
2093 | 78 | crash on SIGINT/KeyboardInterrupt, useful with --pdb | ||
2094 | 79 | """ | ||
2095 | 80 | self.assertEqual(io.combined, cleandoc(expected) + "\n") | ||
2096 | 81 | |||
2097 | 82 | def test_run_without_args(self): | ||
2098 | 83 | with TestIO(combined=True) as io: | ||
2099 | 84 | with self.assertRaises(SystemExit) as call: | ||
2100 | 85 | main([]) | ||
2101 | 86 | self.assertEqual(call.exception.args, (2,)) | ||
2102 | 87 | expected = """ | ||
2103 | 88 | usage: checkbox [-h] [--version] [-v] [-D] [-C] [-T LOGGER] [-P] [-I] | ||
2104 | 89 | {sru,check-config,submit,launcher,self-test} ... | ||
2105 | 90 | checkbox: error: too few arguments | ||
2106 | 91 | |||
2107 | 92 | """ | ||
2108 | 93 | self.assertEqual(io.combined, cleandoc(expected) + "\n") | ||
2109 | diff --git a/checkbox_ng/test_misc.py b/checkbox_ng/test_misc.py | |||
2110 | 94 | deleted file mode 100644 | 0 | deleted file mode 100644 |
2111 | index efb26ac..0000000 | |||
2112 | --- a/checkbox_ng/test_misc.py | |||
2113 | +++ /dev/null | |||
2114 | @@ -1,183 +0,0 @@ | |||
2115 | 1 | # This file is part of Checkbox. | ||
2116 | 2 | # | ||
2117 | 3 | # Copyright 2014 Canonical Ltd. | ||
2118 | 4 | # Written by: | ||
2119 | 5 | # Sylvain Pineau <sylvain.pineau@canonical.com> | ||
2120 | 6 | # | ||
2121 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
2122 | 8 | # it under the terms of the GNU General Public License version 3, | ||
2123 | 9 | # as published by the Free Software Foundation. | ||
2124 | 10 | |||
2125 | 11 | # | ||
2126 | 12 | # Checkbox is distributed in the hope that it will be useful, | ||
2127 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2128 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2129 | 15 | # GNU General Public License for more details. | ||
2130 | 16 | # | ||
2131 | 17 | # You should have received a copy of the GNU General Public License | ||
2132 | 18 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
2133 | 19 | |||
2134 | 20 | """ | ||
2135 | 21 | checkbox_ng.commands.test_cli | ||
2136 | 22 | ============================= | ||
2137 | 23 | |||
2138 | 24 | Test definitions for checkbox_ng.commands.cli module | ||
2139 | 25 | """ | ||
2140 | 26 | |||
2141 | 27 | from unittest import TestCase | ||
2142 | 28 | |||
2143 | 29 | from plainbox.impl.session import SessionState | ||
2144 | 30 | from plainbox.impl.testing_utils import make_job | ||
2145 | 31 | from plainbox.impl.unit.job import JobDefinition | ||
2146 | 32 | |||
2147 | 33 | from checkbox_ng.misc import JobTreeNode | ||
2148 | 34 | from checkbox_ng.misc import SelectableJobTreeNode | ||
2149 | 35 | |||
2150 | 36 | |||
2151 | 37 | class TestJobTreeNode(TestCase): | ||
2152 | 38 | |||
2153 | 39 | def setUp(self): | ||
2154 | 40 | A = make_job('A') | ||
2155 | 41 | B = make_job('B', plugin='local', description='foo') | ||
2156 | 42 | C = make_job('C') | ||
2157 | 43 | D = make_job('D', plugin='shell') | ||
2158 | 44 | E = make_job('E', plugin='local', description='bar') | ||
2159 | 45 | F = make_job('F', plugin='shell') | ||
2160 | 46 | G = make_job('G', plugin='local', description='baz') | ||
2161 | 47 | R = make_job('R', plugin='resource') | ||
2162 | 48 | Z = make_job('Z', plugin='local', description='zaz') | ||
2163 | 49 | state = SessionState([A, B, C, D, E, F, G, R, Z]) | ||
2164 | 50 | # D and E are a child of B | ||
2165 | 51 | state.job_state_map[D.id].via_job = B | ||
2166 | 52 | state.job_state_map[E.id].via_job = B | ||
2167 | 53 | # F is a child of E | ||
2168 | 54 | state.job_state_map[F.id].via_job = E | ||
2169 | 55 | self.tree = JobTreeNode.create_tree( | ||
2170 | 56 | state, [R, B, C, D, E, F, G, A, Z]) | ||
2171 | 57 | |||
2172 | 58 | def test_create_tree(self): | ||
2173 | 59 | self.assertIsInstance(self.tree, JobTreeNode) | ||
2174 | 60 | self.assertEqual(len(self.tree.categories), 3) | ||
2175 | 61 | [self.assertIsInstance(c, JobTreeNode) for c in self.tree.categories] | ||
2176 | 62 | self.assertEqual(len(self.tree.jobs), 3) | ||
2177 | 63 | [self.assertIsInstance(j, JobDefinition) for j in self.tree.jobs] | ||
2178 | 64 | self.assertIsNone(self.tree.parent) | ||
2179 | 65 | self.assertEqual(self.tree.depth, 0) | ||
2180 | 66 | node = self.tree.categories[1] | ||
2181 | 67 | self.assertEqual(node.name, 'foo') | ||
2182 | 68 | self.assertEqual(len(node.categories), 1) | ||
2183 | 69 | [self.assertIsInstance(c, JobTreeNode) for c in node.categories] | ||
2184 | 70 | self.assertEqual(len(node.jobs), 1) | ||
2185 | 71 | [self.assertIsInstance(j, JobDefinition) for j in node.jobs] | ||
2186 | 72 | |||
2187 | 73 | |||
2188 | 74 | class TestSelectableJobTreeNode(TestCase): | ||
2189 | 75 | |||
2190 | 76 | def setUp(self): | ||
2191 | 77 | self.A = make_job('a', name='A') | ||
2192 | 78 | self.B = make_job('b', name='B', plugin='local', description='foo') | ||
2193 | 79 | self.C = make_job('c', name='C') | ||
2194 | 80 | self.D = make_job('d', name='D', plugin='shell') | ||
2195 | 81 | self.E = make_job('e', name='E', plugin='shell') | ||
2196 | 82 | self.F = make_job('f', name='F', plugin='resource', description='baz') | ||
2197 | 83 | state = SessionState([self.A, self.B, self.C, self.D, self.E, self.F]) | ||
2198 | 84 | # D and E are a child of B | ||
2199 | 85 | state.job_state_map[self.D.id].via_job = self.B | ||
2200 | 86 | state.job_state_map[self.E.id].via_job = self.B | ||
2201 | 87 | self.tree = SelectableJobTreeNode.create_tree(state, [ | ||
2202 | 88 | self.A, | ||
2203 | 89 | self.B, | ||
2204 | 90 | self.C, | ||
2205 | 91 | self.D, | ||
2206 | 92 | self.E, | ||
2207 | 93 | self.F | ||
2208 | 94 | ]) | ||
2209 | 95 | |||
2210 | 96 | def test_create_tree(self): | ||
2211 | 97 | self.assertIsInstance(self.tree, SelectableJobTreeNode) | ||
2212 | 98 | self.assertEqual(len(self.tree.categories), 1) | ||
2213 | 99 | [self.assertIsInstance(c, SelectableJobTreeNode) | ||
2214 | 100 | for c in self.tree.categories] | ||
2215 | 101 | self.assertEqual(len(self.tree.jobs), 2) | ||
2216 | 102 | [self.assertIsInstance(j, JobDefinition) for j in self.tree.jobs] | ||
2217 | 103 | self.assertTrue(self.tree.selected) | ||
2218 | 104 | [self.assertTrue(self.tree.job_selection[j]) | ||
2219 | 105 | for j in self.tree.job_selection] | ||
2220 | 106 | self.assertTrue(self.tree.expanded) | ||
2221 | 107 | self.assertIsNone(self.tree.parent) | ||
2222 | 108 | self.assertEqual(self.tree.depth, 0) | ||
2223 | 109 | |||
2224 | 110 | def test_get_node_by_index(self): | ||
2225 | 111 | self.assertEqual(self.tree.get_node_by_index(0)[0].name, 'foo') | ||
2226 | 112 | self.assertEqual(self.tree.get_node_by_index(1)[0].name, 'D') | ||
2227 | 113 | self.assertEqual(self.tree.get_node_by_index(2)[0].name, 'E') | ||
2228 | 114 | self.assertEqual(self.tree.get_node_by_index(3)[0].name, 'A') | ||
2229 | 115 | self.assertEqual(self.tree.get_node_by_index(4)[0].name, 'C') | ||
2230 | 116 | self.assertIsNone(self.tree.get_node_by_index(5)[0]) | ||
2231 | 117 | |||
2232 | 118 | def test_render(self): | ||
2233 | 119 | expected = ['[X] - foo', | ||
2234 | 120 | '[X] d', | ||
2235 | 121 | '[X] e', | ||
2236 | 122 | '[X] a', | ||
2237 | 123 | '[X] c'] | ||
2238 | 124 | self.assertEqual(self.tree.render(), expected) | ||
2239 | 125 | |||
2240 | 126 | def test_render_deselected_all(self): | ||
2241 | 127 | self.tree.set_descendants_state(False) | ||
2242 | 128 | expected = ['[ ] - foo', | ||
2243 | 129 | '[ ] d', | ||
2244 | 130 | '[ ] e', | ||
2245 | 131 | '[ ] a', | ||
2246 | 132 | '[ ] c'] | ||
2247 | 133 | self.assertEqual(self.tree.render(), expected) | ||
2248 | 134 | |||
2249 | 135 | def test_render_reselected_all(self): | ||
2250 | 136 | self.tree.set_descendants_state(False) | ||
2251 | 137 | self.tree.set_descendants_state(True) | ||
2252 | 138 | expected = ['[X] - foo', | ||
2253 | 139 | '[X] d', | ||
2254 | 140 | '[X] e', | ||
2255 | 141 | '[X] a', | ||
2256 | 142 | '[X] c'] | ||
2257 | 143 | self.assertEqual(self.tree.render(), expected) | ||
2258 | 144 | |||
2259 | 145 | def test_render_with_child_collapsed(self): | ||
2260 | 146 | self.tree.categories[0].expanded = False | ||
2261 | 147 | expected = ['[X] + foo', | ||
2262 | 148 | '[X] a', | ||
2263 | 149 | '[X] c'] | ||
2264 | 150 | self.assertEqual(self.tree.render(), expected) | ||
2265 | 151 | |||
2266 | 152 | def test_set_ancestors_state(self): | ||
2267 | 153 | self.tree.set_descendants_state(False) | ||
2268 | 154 | node = self.tree.categories[0] | ||
2269 | 155 | node.job_selection[self.E] = True | ||
2270 | 156 | node.update_selected_state() | ||
2271 | 157 | node.set_ancestors_state(node.selected) | ||
2272 | 158 | expected = ['[X] - foo', | ||
2273 | 159 | '[ ] d', | ||
2274 | 160 | '[X] e', | ||
2275 | 161 | '[ ] a', | ||
2276 | 162 | '[ ] c'] | ||
2277 | 163 | self.assertEqual(self.tree.render(), expected) | ||
2278 | 164 | node.selected = not(node.selected) | ||
2279 | 165 | node.set_ancestors_state(node.selected) | ||
2280 | 166 | node.set_descendants_state(node.selected) | ||
2281 | 167 | expected = ['[ ] - foo', | ||
2282 | 168 | '[ ] d', | ||
2283 | 169 | '[ ] e', | ||
2284 | 170 | '[ ] a', | ||
2285 | 171 | '[ ] c'] | ||
2286 | 172 | self.assertEqual(self.tree.render(), expected) | ||
2287 | 173 | |||
2288 | 174 | def test_selection(self): | ||
2289 | 175 | self.tree.set_descendants_state(False) | ||
2290 | 176 | node = self.tree.categories[0] | ||
2291 | 177 | node.job_selection[self.D] = True | ||
2292 | 178 | node.update_selected_state() | ||
2293 | 179 | node.set_ancestors_state(node.selected) | ||
2294 | 180 | # Note that in addition to the selected (D) test, we need the | ||
2295 | 181 | # tree selection to contain the resource (F), even though the | ||
2296 | 182 | # user never saw it in the previous tests for visual presentation. | ||
2297 | 183 | self.assertEqual(self.tree.selection, [self.D]) | ||
2298 | diff --git a/checkbox_ng/tools.py b/checkbox_ng/tools.py | |||
2299 | 184 | deleted file mode 100644 | 0 | deleted file mode 100644 |
2300 | index 1aa8f37..0000000 | |||
2301 | --- a/checkbox_ng/tools.py | |||
2302 | +++ /dev/null | |||
2303 | @@ -1,146 +0,0 @@ | |||
2304 | 1 | # This file is part of Checkbox. | ||
2305 | 2 | # | ||
2306 | 3 | # Copyright 2012-2015 Canonical Ltd. | ||
2307 | 4 | # Written by: | ||
2308 | 5 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
2309 | 6 | # | ||
2310 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
2311 | 8 | # it under the terms of the GNU General Public License version 3, | ||
2312 | 9 | # as published by the Free Software Foundation. | ||
2313 | 10 | # | ||
2314 | 11 | # Checkbox is distributed in the hope that it will be useful, | ||
2315 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2316 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2317 | 14 | # GNU General Public License for more details. | ||
2318 | 15 | # | ||
2319 | 16 | # You should have received a copy of the GNU General Public License | ||
2320 | 17 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
2321 | 18 | |||
2322 | 19 | """ | ||
2323 | 20 | :mod:`checkbox_ng.tools` -- top-level command line tools | ||
2324 | 21 | ======================================================== | ||
2325 | 22 | """ | ||
2326 | 23 | |||
2327 | 24 | import logging | ||
2328 | 25 | import os | ||
2329 | 26 | |||
2330 | 27 | from plainbox.impl.clitools import SingleCommandToolMixIn | ||
2331 | 28 | from plainbox.impl.clitools import ToolBase | ||
2332 | 29 | from plainbox.impl.commands.cmd_selftest import SelfTestCommand | ||
2333 | 30 | from plainbox.public import get_providers | ||
2334 | 31 | |||
2335 | 32 | from checkbox_ng import __version__ as version | ||
2336 | 33 | from checkbox_ng.config import CheckBoxConfig | ||
2337 | 34 | from checkbox_ng.tests import load_unit_tests | ||
2338 | 35 | |||
2339 | 36 | |||
2340 | 37 | logger = logging.getLogger("checkbox.ng.tools") | ||
2341 | 38 | |||
2342 | 39 | |||
2343 | 40 | class CheckboxToolBase(ToolBase): | ||
2344 | 41 | """ | ||
2345 | 42 | Base class for all checkbox-ng tools. | ||
2346 | 43 | |||
2347 | 44 | This class contains some shared code like configuration, providers, i18n | ||
2348 | 45 | and version handling. | ||
2349 | 46 | """ | ||
2350 | 47 | |||
2351 | 48 | def _load_config(self): | ||
2352 | 49 | return self.get_config_cls().get() | ||
2353 | 50 | |||
2354 | 51 | def _load_providers(self): | ||
2355 | 52 | return get_providers() | ||
2356 | 53 | |||
2357 | 54 | @classmethod | ||
2358 | 55 | def get_exec_version(cls): | ||
2359 | 56 | """ | ||
2360 | 57 | Get the version of the checkbox-ng package | ||
2361 | 58 | """ | ||
2362 | 59 | return version | ||
2363 | 60 | |||
2364 | 61 | @classmethod | ||
2365 | 62 | def get_config_cls(cls): | ||
2366 | 63 | """ | ||
2367 | 64 | Get particular sub-class of the Config class to use | ||
2368 | 65 | """ | ||
2369 | 66 | return CheckBoxConfig | ||
2370 | 67 | |||
2371 | 68 | def get_gettext_domain(self): | ||
2372 | 69 | """ | ||
2373 | 70 | Get the 'checkbox-ng' gettext domain | ||
2374 | 71 | """ | ||
2375 | 72 | return "checkbox-ng" | ||
2376 | 73 | |||
2377 | 74 | def get_locale_dir(self): | ||
2378 | 75 | """ | ||
2379 | 76 | Get an optional development locale directory specific to checkbox-ng | ||
2380 | 77 | """ | ||
2381 | 78 | return os.getenv("CHECKBOX_NG_LOCALE_DIR", None) | ||
2382 | 79 | |||
2383 | 80 | |||
2384 | 81 | class CheckboxTool(CheckboxToolBase): | ||
2385 | 82 | """ | ||
2386 | 83 | Tool that implements the new checkbox command. | ||
2387 | 84 | |||
2388 | 85 | This tool has two sub-commands: | ||
2389 | 86 | |||
2390 | 87 | checkbox sru - to run stable release update testing | ||
2391 | 88 | checkbox check-config - to validate and display system configuration | ||
2392 | 89 | """ | ||
2393 | 90 | |||
2394 | 91 | @classmethod | ||
2395 | 92 | def get_exec_name(cls): | ||
2396 | 93 | return "checkbox" | ||
2397 | 94 | |||
2398 | 95 | def add_subcommands(self, subparsers, early_ns=None): | ||
2399 | 96 | from checkbox_ng.commands.launcher import LauncherCommand | ||
2400 | 97 | from checkbox_ng.commands.sru import SRUCommand | ||
2401 | 98 | from checkbox_ng.commands.submit import SubmitCommand | ||
2402 | 99 | from plainbox.impl.commands.cmd_check_config import CheckConfigCommand | ||
2403 | 100 | SRUCommand( | ||
2404 | 101 | self._load_providers, self._load_config | ||
2405 | 102 | ).register_parser(subparsers) | ||
2406 | 103 | CheckConfigCommand( | ||
2407 | 104 | self._load_config | ||
2408 | 105 | ).register_parser(subparsers) | ||
2409 | 106 | SubmitCommand( | ||
2410 | 107 | self._load_config | ||
2411 | 108 | ).register_parser(subparsers) | ||
2412 | 109 | LauncherCommand( | ||
2413 | 110 | self._load_providers, self._load_config | ||
2414 | 111 | ).register_parser(subparsers) | ||
2415 | 112 | SelfTestCommand(load_unit_tests).register_parser(subparsers) | ||
2416 | 113 | |||
2417 | 114 | |||
2418 | 115 | class CheckboxSubmitTool(SingleCommandToolMixIn, CheckboxToolBase): | ||
2419 | 116 | """ | ||
2420 | 117 | A tool class that implements checkbox-submit. | ||
2421 | 118 | |||
2422 | 119 | This tool implements the submit feature to send test results to the | ||
2423 | 120 | Canonical certification website | ||
2424 | 121 | """ | ||
2425 | 122 | |||
2426 | 123 | @classmethod | ||
2427 | 124 | def get_exec_name(cls): | ||
2428 | 125 | return "checkbox-submit" | ||
2429 | 126 | |||
2430 | 127 | def get_command(self): | ||
2431 | 128 | from checkbox_ng.commands.submit import SubmitCommand | ||
2432 | 129 | return SubmitCommand(self._load_config) | ||
2433 | 130 | |||
2434 | 131 | |||
2435 | 132 | class CheckboxLauncherTool(SingleCommandToolMixIn, CheckboxToolBase): | ||
2436 | 133 | """ | ||
2437 | 134 | A tool class that implements checkbox-launcher. | ||
2438 | 135 | |||
2439 | 136 | This tool implements configurable text-mode-graphics launchers that perform | ||
2440 | 137 | a pre-defined testing session based on the launcher profile. | ||
2441 | 138 | """ | ||
2442 | 139 | |||
2443 | 140 | @classmethod | ||
2444 | 141 | def get_exec_name(cls): | ||
2445 | 142 | return "checkbox-launcher" | ||
2446 | 143 | |||
2447 | 144 | def get_command(self): | ||
2448 | 145 | from checkbox_ng.commands.launcher import LauncherCommand | ||
2449 | 146 | return LauncherCommand(self._load_providers, self._load_config) | ||
2450 | diff --git a/checkbox_ng/ui.py b/checkbox_ng/ui.py | |||
2451 | 147 | deleted file mode 100644 | 0 | deleted file mode 100644 |
2452 | index 322c522..0000000 | |||
2453 | --- a/checkbox_ng/ui.py | |||
2454 | +++ /dev/null | |||
2455 | @@ -1,363 +0,0 @@ | |||
2456 | 1 | # This file is part of Checkbox. | ||
2457 | 2 | # | ||
2458 | 3 | # Copyright 2013-2015 Canonical Ltd. | ||
2459 | 4 | # Written by: | ||
2460 | 5 | # Sylvain Pineau <sylvain.pineau@canonical.com> | ||
2461 | 6 | # | ||
2462 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
2463 | 8 | # it under the terms of the GNU General Public License version 3, | ||
2464 | 9 | # as published by the Free Software Foundation. | ||
2465 | 10 | |||
2466 | 11 | # | ||
2467 | 12 | # Checkbox is distributed in the hope that it will be useful, | ||
2468 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2469 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2470 | 15 | # GNU General Public License for more details. | ||
2471 | 16 | # | ||
2472 | 17 | # You should have received a copy of the GNU General Public License | ||
2473 | 18 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
2474 | 19 | |||
2475 | 20 | """ | ||
2476 | 21 | :mod:`checkbox_ng.ui` -- user interface elements | ||
2477 | 22 | ================================================ | ||
2478 | 23 | """ | ||
2479 | 24 | |||
2480 | 25 | from gettext import gettext as _ | ||
2481 | 26 | from logging import getLogger | ||
2482 | 27 | import textwrap | ||
2483 | 28 | |||
2484 | 29 | from plainbox.vendor.textland import DrawingContext | ||
2485 | 30 | from plainbox.vendor.textland import EVENT_KEYBOARD | ||
2486 | 31 | from plainbox.vendor.textland import EVENT_RESIZE | ||
2487 | 32 | from plainbox.vendor.textland import Event | ||
2488 | 33 | from plainbox.vendor.textland import IApplication | ||
2489 | 34 | from plainbox.vendor.textland import Size | ||
2490 | 35 | from plainbox.vendor.textland import TextImage | ||
2491 | 36 | from plainbox.vendor.textland import NORMAL, REVERSE | ||
2492 | 37 | |||
2493 | 38 | |||
2494 | 39 | logger = getLogger("checkbox.ng.ui") | ||
2495 | 40 | |||
2496 | 41 | |||
2497 | 42 | class ShowWelcome(IApplication): | ||
2498 | 43 | """ | ||
2499 | 44 | Display a welcome message | ||
2500 | 45 | """ | ||
2501 | 46 | def __init__(self, text): | ||
2502 | 47 | self.image = TextImage(Size(0, 0)) | ||
2503 | 48 | self.text = text | ||
2504 | 49 | |||
2505 | 50 | def consume_event(self, event: Event): | ||
2506 | 51 | if event.kind == EVENT_RESIZE: | ||
2507 | 52 | self.image = TextImage(event.data) # data is the new size | ||
2508 | 53 | elif event.kind == EVENT_KEYBOARD and event.data.key == "enter": | ||
2509 | 54 | raise StopIteration | ||
2510 | 55 | self.repaint(event) | ||
2511 | 56 | return self.image | ||
2512 | 57 | |||
2513 | 58 | def repaint(self, event: Event): | ||
2514 | 59 | ctx = DrawingContext(self.image) | ||
2515 | 60 | i = 0 | ||
2516 | 61 | ctx.border() | ||
2517 | 62 | for paragraph in self.text.splitlines(): | ||
2518 | 63 | i += 1 | ||
2519 | 64 | for line in textwrap.fill( | ||
2520 | 65 | paragraph, | ||
2521 | 66 | self.image.size.width - 8, | ||
2522 | 67 | replace_whitespace=False).splitlines(): | ||
2523 | 68 | ctx.move_to(4, i) | ||
2524 | 69 | ctx.print(line) | ||
2525 | 70 | i += 1 | ||
2526 | 71 | ctx.move_to(4, i + 1) | ||
2527 | 72 | ctx.attributes.style = REVERSE | ||
2528 | 73 | ctx.print(_("< Continue >")) | ||
2529 | 74 | |||
2530 | 75 | |||
2531 | 76 | class ShowMenu(IApplication): | ||
2532 | 77 | """ | ||
2533 | 78 | Display the appropriate menu and return the selected options | ||
2534 | 79 | """ | ||
2535 | 80 | def __init__(self, title, menu, selection=[0], multiple_allowed=True): | ||
2536 | 81 | self.image = TextImage(Size(0, 0)) | ||
2537 | 82 | self.title = title | ||
2538 | 83 | self.menu = menu | ||
2539 | 84 | self.option_count = len(menu) | ||
2540 | 85 | self.position = 0 # Zero-based index of the selected menu option | ||
2541 | 86 | self.multiple_allowed = multiple_allowed | ||
2542 | 87 | if self.option_count: | ||
2543 | 88 | self.selection = selection | ||
2544 | 89 | else: | ||
2545 | 90 | self.selection = [] | ||
2546 | 91 | |||
2547 | 92 | def consume_event(self, event: Event): | ||
2548 | 93 | if event.kind == EVENT_RESIZE: | ||
2549 | 94 | self.image = TextImage(event.data) # data is the new size | ||
2550 | 95 | elif event.kind == EVENT_KEYBOARD: | ||
2551 | 96 | if event.data.key == "down": | ||
2552 | 97 | if self.position < self.option_count: | ||
2553 | 98 | self.position += 1 | ||
2554 | 99 | else: | ||
2555 | 100 | self.position = 0 | ||
2556 | 101 | elif event.data.key == "up": | ||
2557 | 102 | if self.position > 0: | ||
2558 | 103 | self.position -= 1 | ||
2559 | 104 | else: | ||
2560 | 105 | self.position = self.option_count | ||
2561 | 106 | elif (event.data.key == "enter" and | ||
2562 | 107 | self.position == self.option_count): | ||
2563 | 108 | raise StopIteration(self.selection) | ||
2564 | 109 | elif event.data.key == "space": | ||
2565 | 110 | if self.position in self.selection: | ||
2566 | 111 | self.selection.remove(self.position) | ||
2567 | 112 | elif self.position < self.option_count: | ||
2568 | 113 | self.selection.append(self.position) | ||
2569 | 114 | if not self.multiple_allowed: | ||
2570 | 115 | self.selection = [self.position] | ||
2571 | 116 | self.repaint(event) | ||
2572 | 117 | return self.image | ||
2573 | 118 | |||
2574 | 119 | def repaint(self, event: Event): | ||
2575 | 120 | ctx = DrawingContext(self.image) | ||
2576 | 121 | ctx.border(tm=1) | ||
2577 | 122 | ctx.attributes.style = REVERSE | ||
2578 | 123 | ctx.print(' ' * self.image.size.width) | ||
2579 | 124 | ctx.move_to(1, 0) | ||
2580 | 125 | ctx.print(self.title) | ||
2581 | 126 | |||
2582 | 127 | # Display all the menu items | ||
2583 | 128 | for i in range(self.option_count): | ||
2584 | 129 | ctx.attributes.style = NORMAL | ||
2585 | 130 | if i == self.position: | ||
2586 | 131 | ctx.attributes.style = REVERSE | ||
2587 | 132 | # Display options from line 3, column 4 | ||
2588 | 133 | ctx.move_to(4, 3 + i) | ||
2589 | 134 | ctx.print("[{}] - {}".format( | ||
2590 | 135 | 'X' if i in self.selection else ' ', | ||
2591 | 136 | self.menu[i].replace('ihv-', ''))) | ||
2592 | 137 | |||
2593 | 138 | # Display "OK" at bottom of menu | ||
2594 | 139 | ctx.attributes.style = NORMAL | ||
2595 | 140 | if self.position == self.option_count: | ||
2596 | 141 | ctx.attributes.style = REVERSE | ||
2597 | 142 | # Add an empty line before the last option | ||
2598 | 143 | ctx.move_to(4, 4 + self.option_count) | ||
2599 | 144 | ctx.print("< OK >") | ||
2600 | 145 | |||
2601 | 146 | |||
2602 | 147 | class ScrollableTreeNode(IApplication): | ||
2603 | 148 | """ | ||
2604 | 149 | Class used to interact with a SelectableJobTreeNode | ||
2605 | 150 | """ | ||
2606 | 151 | def __init__(self, tree, title): | ||
2607 | 152 | self.image = TextImage(Size(0, 0)) | ||
2608 | 153 | self.tree = tree | ||
2609 | 154 | self.title = title | ||
2610 | 155 | self.top = 0 # Top line number | ||
2611 | 156 | self.highlight = 0 # Highlighted line number | ||
2612 | 157 | self.summary = True | ||
2613 | 158 | |||
2614 | 159 | def consume_event(self, event: Event): | ||
2615 | 160 | if event.kind == EVENT_RESIZE: | ||
2616 | 161 | self.image = TextImage(event.data) # data is the new size | ||
2617 | 162 | elif event.kind == EVENT_KEYBOARD: | ||
2618 | 163 | self.image = TextImage(self.image.size) | ||
2619 | 164 | if event.data.key == "up": | ||
2620 | 165 | self._scroll("up") | ||
2621 | 166 | elif event.data.key == "down": | ||
2622 | 167 | self._scroll("down") | ||
2623 | 168 | elif event.data.key == "space": | ||
2624 | 169 | self._selectNode() | ||
2625 | 170 | elif event.data.key == "enter": | ||
2626 | 171 | self._toggleNode() | ||
2627 | 172 | elif event.data.key in 'sS': | ||
2628 | 173 | self.tree.set_descendants_state(True) | ||
2629 | 174 | elif event.data.key in 'dD': | ||
2630 | 175 | self.tree.set_descendants_state(False) | ||
2631 | 176 | elif event.data.key in 'iI': | ||
2632 | 177 | self.summary = not self.summary | ||
2633 | 178 | elif event.data.key in 'tT': | ||
2634 | 179 | raise StopIteration | ||
2635 | 180 | self.repaint(event) | ||
2636 | 181 | return self.image | ||
2637 | 182 | |||
2638 | 183 | def repaint(self, event: Event): | ||
2639 | 184 | ctx = DrawingContext(self.image) | ||
2640 | 185 | ctx.border(tm=1, bm=1) | ||
2641 | 186 | cols = self.image.size.width | ||
2642 | 187 | extra_cols = 0 | ||
2643 | 188 | if cols > 80: | ||
2644 | 189 | extra_cols = cols - 80 | ||
2645 | 190 | ctx.attributes.style = REVERSE | ||
2646 | 191 | ctx.print(' ' * cols) | ||
2647 | 192 | ctx.move_to(1, 0) | ||
2648 | 193 | bottom = self.top + self.image.size.height - 4 | ||
2649 | 194 | ctx.print(self.title) | ||
2650 | 195 | ctx.move_to(1, self.image.size.height - 1) | ||
2651 | 196 | ctx.attributes.style = REVERSE | ||
2652 | 197 | ctx.print(_("Enter")) | ||
2653 | 198 | ctx.move_to(6, self.image.size.height - 1) | ||
2654 | 199 | ctx.attributes.style = NORMAL | ||
2655 | 200 | ctx.print(_(": Expand/Collapse")) | ||
2656 | 201 | ctx.move_to(27, self.image.size.height - 1) | ||
2657 | 202 | ctx.attributes.style = REVERSE | ||
2658 | 203 | # FIXME: i18n problem | ||
2659 | 204 | ctx.print("S") | ||
2660 | 205 | ctx.move_to(28, self.image.size.height - 1) | ||
2661 | 206 | ctx.attributes.style = NORMAL | ||
2662 | 207 | ctx.print("elect All") | ||
2663 | 208 | ctx.move_to(41, self.image.size.height - 1) | ||
2664 | 209 | ctx.attributes.style = REVERSE | ||
2665 | 210 | # FIXME: i18n problem | ||
2666 | 211 | ctx.print("D") | ||
2667 | 212 | ctx.move_to(42, self.image.size.height - 1) | ||
2668 | 213 | ctx.attributes.style = NORMAL | ||
2669 | 214 | ctx.print("eselect All") | ||
2670 | 215 | ctx.move_to(66 + extra_cols, self.image.size.height - 1) | ||
2671 | 216 | ctx.print(_("Start ")) | ||
2672 | 217 | ctx.move_to(72 + extra_cols, self.image.size.height - 1) | ||
2673 | 218 | ctx.attributes.style = REVERSE | ||
2674 | 219 | # FIXME: i18n problem | ||
2675 | 220 | ctx.print("T") | ||
2676 | 221 | ctx.move_to(73 + extra_cols, self.image.size.height - 1) | ||
2677 | 222 | ctx.attributes.style = NORMAL | ||
2678 | 223 | ctx.print("esting") | ||
2679 | 224 | for i, line in enumerate(self.tree.render(cols - 3, | ||
2680 | 225 | as_summary=self.summary)[self.top:bottom]): | ||
2681 | 226 | ctx.move_to(2, i + 2) | ||
2682 | 227 | if i != self.highlight: | ||
2683 | 228 | ctx.attributes.style = NORMAL | ||
2684 | 229 | else: # highlight the current line | ||
2685 | 230 | ctx.attributes.style = REVERSE | ||
2686 | 231 | ctx.print(line) | ||
2687 | 232 | |||
2688 | 233 | def _selectNode(self): | ||
2689 | 234 | """ | ||
2690 | 235 | Mark a node/job as selected for this test run. | ||
2691 | 236 | See :meth:`SelectableJobTreeNode.set_ancestors_state()` and | ||
2692 | 237 | :meth:`SelectableJobTreeNode.set_descendants_state()` for details | ||
2693 | 238 | about the automatic selection of parents and descendants. | ||
2694 | 239 | """ | ||
2695 | 240 | node, category = self.tree.get_node_by_index(self.top + self.highlight) | ||
2696 | 241 | if category: # then the selected node is a job not a category | ||
2697 | 242 | job = node | ||
2698 | 243 | category.job_selection[job] = not(category.job_selection[job]) | ||
2699 | 244 | category.update_selected_state() | ||
2700 | 245 | category.set_ancestors_state(category.job_selection[job]) | ||
2701 | 246 | else: | ||
2702 | 247 | node.selected = not(node.selected) | ||
2703 | 248 | node.set_descendants_state(node.selected) | ||
2704 | 249 | node.set_ancestors_state(node.selected) | ||
2705 | 250 | |||
2706 | 251 | def _toggleNode(self): | ||
2707 | 252 | """ | ||
2708 | 253 | Expand/collapse a node | ||
2709 | 254 | """ | ||
2710 | 255 | node, is_job = self.tree.get_node_by_index(self.top + self.highlight) | ||
2711 | 256 | if node is not None and not is_job: | ||
2712 | 257 | node.expanded = not(node.expanded) | ||
2713 | 258 | |||
2714 | 259 | def _scroll(self, direction): | ||
2715 | 260 | visible_length = len(self.tree) | ||
2716 | 261 | # Scroll the tree view | ||
2717 | 262 | if (direction == "up" and | ||
2718 | 263 | self.highlight == 0 and self.top != 0): | ||
2719 | 264 | self.top -= 1 | ||
2720 | 265 | return | ||
2721 | 266 | elif (direction == "down" and | ||
2722 | 267 | (self.highlight + 1) == (self.image.size.height - 4) and | ||
2723 | 268 | (self.top + self.image.size.height - 4) != visible_length): | ||
2724 | 269 | self.top += 1 | ||
2725 | 270 | return | ||
2726 | 271 | # Move the highlighted line | ||
2727 | 272 | if (direction == "up" and | ||
2728 | 273 | (self.top != 0 or self.highlight != 0)): | ||
2729 | 274 | self.highlight -= 1 | ||
2730 | 275 | elif (direction == "down" and | ||
2731 | 276 | (self.top + self.highlight + 1) != visible_length and | ||
2732 | 277 | (self.highlight + 1) != (self.image.size.height - 4)): | ||
2733 | 278 | self.highlight += 1 | ||
2734 | 279 | |||
2735 | 280 | |||
2736 | 281 | class ShowRerun(ScrollableTreeNode): | ||
2737 | 282 | """ Display the re-run screen.""" | ||
2738 | 283 | def __init__(self, tree, title): | ||
2739 | 284 | super().__init__(tree, title) | ||
2740 | 285 | |||
2741 | 286 | def consume_event(self, event: Event): | ||
2742 | 287 | if event.kind == EVENT_RESIZE: | ||
2743 | 288 | self.image = TextImage(event.data) # data is the new size | ||
2744 | 289 | elif event.kind == EVENT_KEYBOARD: | ||
2745 | 290 | self.image = TextImage(self.image.size) | ||
2746 | 291 | if event.data.key == "up": | ||
2747 | 292 | self._scroll("up") | ||
2748 | 293 | elif event.data.key == "down": | ||
2749 | 294 | self._scroll("down") | ||
2750 | 295 | elif event.data.key == "space": | ||
2751 | 296 | self._selectNode() | ||
2752 | 297 | elif event.data.key == "enter": | ||
2753 | 298 | self._toggleNode() | ||
2754 | 299 | elif event.data.key in 'sS': | ||
2755 | 300 | self.tree.set_descendants_state(True) | ||
2756 | 301 | elif event.data.key in 'dD': | ||
2757 | 302 | self.tree.set_descendants_state(False) | ||
2758 | 303 | elif event.data.key in 'fF': | ||
2759 | 304 | self.tree.set_descendants_state(False) | ||
2760 | 305 | raise StopIteration | ||
2761 | 306 | elif event.data.key in 'rR': | ||
2762 | 307 | raise StopIteration | ||
2763 | 308 | self.repaint(event) | ||
2764 | 309 | return self.image | ||
2765 | 310 | |||
2766 | 311 | def repaint(self, event: Event): | ||
2767 | 312 | ctx = DrawingContext(self.image) | ||
2768 | 313 | ctx.border(tm=1, bm=1) | ||
2769 | 314 | cols = self.image.size.width | ||
2770 | 315 | extra_cols = 0 | ||
2771 | 316 | if cols > 80: | ||
2772 | 317 | extra_cols = cols - 80 | ||
2773 | 318 | ctx.attributes.style = REVERSE | ||
2774 | 319 | ctx.print(' ' * cols) | ||
2775 | 320 | ctx.move_to(1, 0) | ||
2776 | 321 | bottom = self.top + self.image.size.height - 4 | ||
2777 | 322 | ctx.print(self.title) | ||
2778 | 323 | ctx.move_to(1, self.image.size.height - 1) | ||
2779 | 324 | ctx.attributes.style = REVERSE | ||
2780 | 325 | ctx.print(_("Enter")) | ||
2781 | 326 | ctx.move_to(6, self.image.size.height - 1) | ||
2782 | 327 | ctx.attributes.style = NORMAL | ||
2783 | 328 | ctx.print(_(": Expand/Collapse")) | ||
2784 | 329 | ctx.move_to(27, self.image.size.height - 1) | ||
2785 | 330 | ctx.attributes.style = REVERSE | ||
2786 | 331 | # FIXME: i18n problem | ||
2787 | 332 | ctx.print("S") | ||
2788 | 333 | ctx.move_to(28, self.image.size.height - 1) | ||
2789 | 334 | ctx.attributes.style = NORMAL | ||
2790 | 335 | ctx.print("elect All") | ||
2791 | 336 | ctx.move_to(41, self.image.size.height - 1) | ||
2792 | 337 | ctx.attributes.style = REVERSE | ||
2793 | 338 | # FIXME: i18n problem | ||
2794 | 339 | ctx.print("D") | ||
2795 | 340 | ctx.move_to(42, self.image.size.height - 1) | ||
2796 | 341 | ctx.attributes.style = NORMAL | ||
2797 | 342 | ctx.print("eselect All") | ||
2798 | 343 | ctx.move_to(63 + extra_cols, self.image.size.height - 1) | ||
2799 | 344 | ctx.attributes.style = REVERSE | ||
2800 | 345 | # FIXME: i18n problem | ||
2801 | 346 | ctx.print("F") | ||
2802 | 347 | ctx.move_to(64 + extra_cols, self.image.size.height - 1) | ||
2803 | 348 | ctx.attributes.style = NORMAL | ||
2804 | 349 | ctx.print(_("inish")) | ||
2805 | 350 | ctx.move_to(73 + extra_cols, self.image.size.height - 1) | ||
2806 | 351 | ctx.attributes.style = REVERSE | ||
2807 | 352 | # FIXME: i18n problem | ||
2808 | 353 | ctx.print("R") | ||
2809 | 354 | ctx.move_to(74 + extra_cols, self.image.size.height - 1) | ||
2810 | 355 | ctx.attributes.style = NORMAL | ||
2811 | 356 | ctx.print("e-run") | ||
2812 | 357 | for i, line in enumerate(self.tree.render(cols - 3)[self.top:bottom]): | ||
2813 | 358 | ctx.move_to(2, i + 2) | ||
2814 | 359 | if i != self.highlight: | ||
2815 | 360 | ctx.attributes.style = NORMAL | ||
2816 | 361 | else: # highlight the current line | ||
2817 | 362 | ctx.attributes.style = REVERSE | ||
2818 | 363 | ctx.print(line) | ||
2819 | diff --git a/po/POTFILES.in b/po/POTFILES.in | |||
2820 | index c9804d5..8934fc2 100644 | |||
2821 | --- a/po/POTFILES.in | |||
2822 | +++ b/po/POTFILES.in | |||
2823 | @@ -1,19 +1,13 @@ | |||
2824 | 1 | [encoding: UTF-8] | 1 | [encoding: UTF-8] |
2825 | 2 | ./docs/conf.py | ||
2826 | 2 | ./checkbox_ng/__init__.py | 3 | ./checkbox_ng/__init__.py |
2827 | 3 | ./checkbox_ng/certification.py | 4 | ./checkbox_ng/certification.py |
2828 | 4 | ./checkbox_ng/commands/__init__.py | ||
2829 | 5 | ./checkbox_ng/commands/cli.py | ||
2830 | 6 | ./checkbox_ng/commands/launcher.py | ||
2831 | 7 | ./checkbox_ng/commands/newcli.py | ||
2832 | 8 | ./checkbox_ng/commands/sru.py | ||
2833 | 9 | ./checkbox_ng/commands/submit.py | ||
2834 | 10 | ./checkbox_ng/commands/test_sru.py | ||
2835 | 11 | ./checkbox_ng/config.py | 5 | ./checkbox_ng/config.py |
2838 | 12 | ./checkbox_ng/launchpad.py | 6 | ./checkbox_ng/launcher/__init__.py |
2839 | 13 | ./checkbox_ng/main.py | 7 | ./checkbox_ng/launcher/checkbox_cli.py |
2840 | 8 | ./checkbox_ng/launcher/stages.py | ||
2841 | 9 | ./checkbox_ng/launcher/subcommands.py | ||
2842 | 14 | ./checkbox_ng/test_certification.py | 10 | ./checkbox_ng/test_certification.py |
2843 | 15 | ./checkbox_ng/test_config.py | ||
2844 | 16 | ./checkbox_ng/test_main.py | ||
2845 | 17 | ./checkbox_ng/test_misc.py | ||
2846 | 18 | ./checkbox_ng/tests.py | 11 | ./checkbox_ng/tests.py |
2848 | 19 | ./checkbox_ng/ui.py | 12 | ./checkbox_ng/urwid_ui.py |
2849 | 13 | ./setup.py | ||
2850 | diff --git a/setup.py b/setup.py | |||
2851 | index b6226bf..f4ddbf5 100755 | |||
2852 | --- a/setup.py | |||
2853 | +++ b/setup.py | |||
2854 | @@ -68,9 +68,6 @@ setup( | |||
2855 | 68 | entry_points={ | 68 | entry_points={ |
2856 | 69 | 'console_scripts': [ | 69 | 'console_scripts': [ |
2857 | 70 | 'checkbox-cli=checkbox_ng.launcher.checkbox_cli:main', | 70 | 'checkbox-cli=checkbox_ng.launcher.checkbox_cli:main', |
2858 | 71 | 'checkbox=checkbox_ng.main:main', | ||
2859 | 72 | 'checkbox-submit=checkbox_ng.main:submit', | ||
2860 | 73 | 'checkbox-launcher=checkbox_ng.main:launcher', | ||
2861 | 74 | ], | 71 | ], |
2862 | 75 | 'plainbox.transport': [ | 72 | 'plainbox.transport': [ |
2863 | 76 | 'certification=' | 73 | 'certification=' |
As of 61decd1 commit everything looks (and works) good.
I wonder if there are any configurations using the old syntax... Guess we'll learn after this lands :-)