Merge ~sylvain-pineau/checkbox-ng:drop-legacy-commands into checkbox-ng:master

Proposed by Sylvain Pineau
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)
Reviewer Review Type Date Requested Status
Taihsiang Ho Approve
Maciej Kisielewski (community) Approve
Review via email: mp+325213@code.launchpad.net

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/checkbox.conf).
- 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)

To post a comment you must log in.
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

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 :-)

review: Approve
Revision history for this message
Taihsiang Ho (tai271828) wrote :
review: Approve
Revision history for this message
Taihsiang Ho (tai271828) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/checkbox_ng/commands/__init__.py b/checkbox_ng/commands/__init__.py
0deleted file mode 1006440deleted file mode 100644
index a7ea4d3..0000000
--- a/checkbox_ng/commands/__init__.py
+++ /dev/null
@@ -1,63 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2014 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10#
11# Checkbox is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
18
19"""
20:mod:`checkbox_ng.commands` -- shared code for checkbox-ng sub-commands
21=======================================================================
22"""
23
24from plainbox.impl.clitools import CommandBase
25
26
27class CheckboxCommand(CommandBase):
28 """
29 Simple interface class for checkbox-ng commands.
30
31 Command objects like this are consumed by CheckBoxNGTool subclasses to
32 implement hierarchical command system. The API supports arbitrary many sub
33 commands in arbitrary nesting arrangement.
34 """
35
36 gettext_domain = "checkbox-ng"
37
38 def __init__(self, provider_loader, config_loader):
39 """
40 Initialize a command with the specified arguments.
41
42 :param provider_loader:
43 A callable returning a list of Provider1 objects
44 :param config_loader:
45 A callable returning a Config object
46 """
47 self._provider_loader = provider_loader
48 self._config_loader = config_loader
49
50 @property
51 def provider_loader(self):
52 """
53 a callable returning a list of PlainBox providers associated with this
54 command
55 """
56 return self._provider_loader
57
58 @property
59 def config_loader(self):
60 """
61 a callable returning a Config object
62 """
63 return self._config_loader
diff --git a/checkbox_ng/commands/cli.py b/checkbox_ng/commands/cli.py
64deleted file mode 1006440deleted file mode 100644
index e7d6c45..0000000
--- a/checkbox_ng/commands/cli.py
+++ /dev/null
@@ -1,82 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013-2014 Canonical Ltd.
4# Written by:
5# Sylvain Pineau <sylvain.pineau@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10#
11# Checkbox is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
18
19"""
20:mod:`checkbox_ng.commands.cli` -- Command line sub-command
21===========================================================
22
23.. warning::
24
25 THIS MODULE DOES NOT HAVE STABLE PUBLIC API
26"""
27
28from argparse import SUPPRESS
29from gettext import gettext as _
30from logging import getLogger
31
32from plainbox.impl.commands import PlainBoxCommand
33from plainbox.impl.commands.cmd_checkbox import CheckBoxCommandMixIn
34from plainbox.impl.commands.inv_check_config import CheckConfigInvocation
35
36from checkbox_ng.commands.newcli import CliInvocation2
37
38
39logger = getLogger("checkbox.ng.commands.cli")
40
41
42class CliCommand(PlainBoxCommand, CheckBoxCommandMixIn):
43 """
44 Command for running tests using the command line UI.
45 """
46 gettext_domain = "checkbox-ng"
47
48 def __init__(self, provider_loader, config_loader, settings):
49 self.provider_loader = provider_loader
50 self.config_loader = config_loader
51 self.settings = settings
52
53 def invoked(self, ns):
54 # Run check-config, if requested
55 if ns.check_config:
56 retval = CheckConfigInvocation(self.config_loader).run()
57 return retval
58 return CliInvocation2(
59 self.provider_loader, self.loader_config, ns, self.settings
60 ).run()
61
62 def register_parser(self, subparsers):
63 parser = subparsers.add_parser(self.settings['subparser_name'],
64 help=self.settings['subparser_help'])
65 parser.set_defaults(command=self)
66 parser.set_defaults(dry_run=False)
67 parser.add_argument(
68 "--check-config",
69 action="store_true",
70 help=_("run check-config"))
71 group = parser.add_argument_group(title=_("user interface options"))
72 parser.set_defaults(color=None)
73 group.add_argument(
74 '--no-color', dest='color', action='store_false', help=SUPPRESS)
75 group.add_argument(
76 '--non-interactive', action='store_true',
77 help=_("skip tests that require interactivity"))
78 group.add_argument(
79 '--dont-suppress-output', action="store_true", default=False,
80 help=_("don't suppress the output of certain job plugin types"))
81 # Call enhance_parser from CheckBoxCommandMixIn
82 self.enhance_parser(parser)
diff --git a/checkbox_ng/commands/launcher.py b/checkbox_ng/commands/launcher.py
83deleted file mode 1006440deleted file mode 100644
index 87eb180..0000000
--- a/checkbox_ng/commands/launcher.py
+++ /dev/null
@@ -1,115 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2014 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21:mod:`checkbox_ng.commands.launcher` -- `checkbox launcher` command
22===================================================================
23"""
24
25from argparse import SUPPRESS
26from gettext import gettext as _
27import itertools
28import logging
29import os
30
31from checkbox_ng.commands import CheckboxCommand
32from checkbox_ng.commands.newcli import CliInvocation2
33from checkbox_ng.commands.submit import SubmitCommand
34from checkbox_ng.config import CheckBoxConfig
35
36from plainbox.impl.commands.cmd_checkbox import CheckBoxCommandMixIn
37from plainbox.impl.launcher import LauncherDefinition
38
39logger = logging.getLogger("checkbox.ng.commands.launcher")
40
41
42class LauncherCommand(CheckboxCommand, CheckBoxCommandMixIn, SubmitCommand):
43 """
44 run a customized testing session
45
46 This command can be used as an interpreter for the so-called launchers.
47 Those launchers are small text files that define the parameters of the test
48 and can be executed directly to run a customized checkbox-ng testing
49 session.
50 """
51
52 def __init__(self, provider_loader, config_loader):
53 self._provider_loader = provider_loader
54 self.config = config_loader()
55
56 def invoked(self, ns):
57 try:
58 with open(ns.launcher, 'rt', encoding='UTF-8') as stream:
59 first_line = stream.readline()
60 if not first_line.startswith("#!"):
61 stream.seek(0)
62 text = stream.read()
63 except IOError as exc:
64 logger.error(_("Unable to load launcher definition: %s"), exc)
65 return 1
66 generic_launcher = LauncherDefinition()
67 generic_launcher.read_string(text)
68 launcher = generic_launcher.get_concrete_launcher()
69 launcher.read_string(text)
70 if launcher.problem_list:
71 logger.error(_("Unable to start launcher because of errors:"))
72 for problem in launcher.problem_list:
73 logger.error("%s", str(problem))
74 return 1
75 # Override the default CheckBox configuration with the one provided
76 # by the launcher
77 self.config.Meta.filename_list = list(
78 itertools.chain(
79 *zip(
80 itertools.islice(
81 CheckBoxConfig.Meta.filename_list, 0, None, 2),
82 itertools.islice(
83 CheckBoxConfig.Meta.filename_list, 1, None, 2),
84 ('/etc/xdg/{}'.format(launcher.config_filename),
85 os.path.expanduser(
86 '~/.config/{}'.format(launcher.config_filename)))))
87 )
88 self.config.read(self.config.Meta.filename_list)
89 ns.dry_run = False
90 ns.dont_suppress_output = launcher.dont_suppress_output
91 return CliInvocation2(
92 self.provider_loader, lambda: self.config, ns, launcher
93 ).run()
94
95 def register_parser(self, subparsers):
96 parser = self.add_subcommand(subparsers)
97 self.register_arguments(parser)
98
99 def register_arguments(self, parser):
100 parser.add_argument(
101 '--no-color', dest='color', action='store_false', help=SUPPRESS)
102 parser.set_defaults(color=None)
103 parser.add_argument(
104 "launcher", metavar=_("LAUNCHER"),
105 help=_("launcher definition file to use"))
106 parser.set_defaults(command=self)
107 parser.conflict_handler = 'resolve'
108 # Call enhance_parser from CheckBoxCommandMixIn
109 self.enhance_parser(parser)
110 group = parser.add_argument_group(title=_("user interface options"))
111 group.add_argument(
112 '--non-interactive', action='store_true',
113 help=_("skip tests that require interactivity"))
114 # Call register_optional_arguments from SubmitCommand
115 self.register_optional_arguments(parser)
diff --git a/checkbox_ng/commands/newcli.py b/checkbox_ng/commands/newcli.py
116deleted file mode 1006440deleted file mode 100644
index 8d359c0..0000000
--- a/checkbox_ng/commands/newcli.py
+++ /dev/null
@@ -1,491 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013-2014 Canonical Ltd.
4# Written by:
5# Sylvain Pineau <sylvain.pineau@canonical.com>
6# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
7#
8# Checkbox is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 3,
10# as published by the Free Software Foundation.
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21:mod:`checkbox_ng.commands.cli` -- Command line sub-command
22===========================================================
23
24.. warning::
25
26 THIS MODULE DOES NOT HAVE STABLE PUBLIC API
27"""
28
29from gettext import gettext as _
30from logging import getLogger
31from shutil import copyfileobj
32import io
33import operator
34import os
35import re
36import subprocess
37import sys
38
39from plainbox.abc import IJobResult
40from plainbox.impl.commands.inv_run import RunInvocation
41from plainbox.impl.exporter import ByteStringStreamTranslator
42from plainbox.impl.secure.config import Unset, ValidationError
43from plainbox.impl.secure.origin import CommandLineTextSource
44from plainbox.impl.secure.origin import Origin
45from plainbox.impl.secure.qualifiers import FieldQualifier
46from plainbox.impl.secure.qualifiers import OperatorMatcher
47from plainbox.impl.secure.qualifiers import RegExpJobQualifier
48from plainbox.impl.secure.qualifiers import select_jobs
49from plainbox.impl.session import SessionMetaData
50from plainbox.impl.session.jobs import InhibitionCause
51from plainbox.impl.transport import TransportError
52from plainbox.impl.transport import get_all_transports
53from plainbox.vendor.textland import get_display
54
55from checkbox_ng.misc import SelectableJobTreeNode
56from checkbox_ng.ui import ScrollableTreeNode
57from checkbox_ng.ui import ShowMenu
58from checkbox_ng.ui import ShowRerun
59from checkbox_ng.ui import ShowWelcome
60
61
62logger = getLogger("checkbox.ng.commands.newcli")
63
64
65class CliInvocation2(RunInvocation):
66 """
67 Invocation of the 'checkbox cli' command.
68
69 :ivar ns:
70 The argparse namespace obtained from CliCommand
71 :ivar _launcher:
72 launcher specific to 'checkbox cli'
73 :ivar _display:
74 A textland display object
75 :ivar _qualifier_list:
76 A list of job qualifiers used to build the session desired_job_list
77 """
78
79 def __init__(self, provider_loader, config_loader, ns, launcher,
80 display=None):
81 super().__init__(provider_loader, config_loader, ns, ns.color)
82 if display is None:
83 display = get_display()
84 self._launcher = launcher
85 self._display = display
86 self._qualifier_list = []
87 self._testplan_list = []
88 self.select_qualifier_list()
89 # MAAS-deployed server images need "tput reset" to keep ugliness
90 # from happening....
91 subprocess.check_call(['tput', 'reset'])
92
93 @property
94 def launcher(self):
95 """
96 TBD: 'checkbox cli' specific launcher settings
97 """
98 return self._launcher
99
100 @property
101 def display(self):
102 """
103 A TextLand display object
104 """
105 return self._display
106
107 def select_qualifier_list(self):
108 # Add whitelists
109 if 'whitelist' in self.ns and self.ns.whitelist:
110 for whitelist_file in self.ns.whitelist:
111 qualifier = self.get_whitelist_from_file(
112 whitelist_file.name, whitelist_file)
113 if qualifier is not None:
114 self._qualifier_list.append(qualifier)
115 # Add all the --include jobs
116 for pattern in self.ns.include_pattern_list:
117 origin = Origin(CommandLineTextSource('-i', pattern), None, None)
118 try:
119 qualifier = RegExpJobQualifier(
120 '^{}$'.format(pattern), origin, inclusive=True)
121 except Exception as exc:
122 logger.warning(
123 _("Incorrect pattern %r: %s"), pattern, exc)
124 else:
125 self._qualifier_list.append(qualifier)
126 # Add all the --exclude jobs
127 for pattern in self.ns.exclude_pattern_list:
128 origin = Origin(CommandLineTextSource('-x', pattern), None, None)
129 try:
130 qualifier = RegExpJobQualifier(
131 '^{}$'.format(pattern), origin, inclusive=False)
132 except Exception as exc:
133 logger.warning(
134 _("Incorrect pattern %r: %s"), pattern, exc)
135 else:
136 self._qualifier_list.append(qualifier)
137 if self.config.whitelist is not Unset:
138 self._qualifier_list.append(
139 self.get_whitelist_from_file(self.config.whitelist))
140
141 def select_testplan(self):
142 # Add the test plan
143 if self.ns.test_plan is not None:
144 for provider in self.provider_list:
145 for unit in provider.id_map[self.ns.test_plan]:
146 if unit.Meta.name == 'test plan':
147 self._qualifier_list.append(unit.get_qualifier())
148 self._testplan_list.append(unit)
149 return
150 else:
151 logger.error(_("There is no test plan: %s"), self.ns.test_plan)
152
153 def run(self):
154 return self.do_normal_sequence()
155
156 def do_normal_sequence(self):
157 """
158 Proceed through normal set of steps that are required to runs jobs
159
160 .. note::
161 This version is overridden as there is no better way to manage this
162 pile rather than having a copy-paste + edits piece of text until
163 arrowhead replaced plainbox run internals with a flow chart that
164 can be derived meaningfully.
165
166 For now just look for changes as compared to run.py's version.
167 """
168 # Create transport early so that we can handle bugs before starting the
169 # session.
170 self.create_transport()
171 if self.is_interactive:
172 resumed = self.maybe_resume_session()
173 else:
174 self.create_manager(None)
175 resumed = False
176 # XXX: we don't want to know about new jobs just yet
177 self.state.on_job_added.disconnect(self.on_job_added)
178 # Create the job runner so that we can do stuff
179 self.create_runner()
180 # If we haven't resumed then do some one-time initialization
181 if not resumed:
182 # Show the welcome message
183 self.show_welcome_screen()
184 # Process testplan command line options
185 self.select_testplan()
186 # Maybe allow the user to do a manual whitelist selection
187 if not self._qualifier_list:
188 self.maybe_interactively_select_testplans()
189 if self._testplan_list:
190 self.manager.test_plans = tuple(self._testplan_list)
191 # Store the application-identifying meta-data and checkpoint the
192 # session.
193 self.store_application_metadata()
194 self.metadata.flags.add(SessionMetaData.FLAG_INCOMPLETE)
195 self.manager.checkpoint()
196 # Run all the local jobs. We need to do this to see all the things
197 # the user may select
198 if self.is_interactive:
199 self.select_local_jobs()
200 self.run_all_selected_jobs()
201 self.interactively_pick_jobs_to_run()
202 # Maybe ask the secure launcher to prompt for the password now. This is
203 # imperfect as we are going to run local jobs and we cannot see if they
204 # might need root or not. This cannot be fixed before template jobs are
205 # added and local jobs deprecated and removed (at least not being a
206 # part of the session we want to execute).
207 self.maybe_warm_up_authentication()
208 self.print_estimated_duration()
209 self.run_all_selected_jobs()
210 if self.is_interactive:
211 while True:
212 if self.maybe_rerun_jobs():
213 continue
214 else:
215 break
216 self.export_and_send_results()
217 if SessionMetaData.FLAG_INCOMPLETE in self.metadata.flags:
218 print(self.C.header("Session Complete!", "GREEN"))
219 self.metadata.flags.remove(SessionMetaData.FLAG_INCOMPLETE)
220 self.manager.checkpoint()
221 return 0
222
223 def store_application_metadata(self):
224 super().store_application_metadata()
225 self.metadata.app_blob = b''
226
227 def show_welcome_screen(self):
228 text = self.launcher.text
229 if self.is_interactive and text:
230 self.display.run(ShowWelcome(text))
231
232 def maybe_interactively_select_testplans(self):
233 if self.launcher.skip_whitelist_selection:
234 self._qualifier_list.extend(self.get_default_testplans())
235 elif self.is_interactive:
236 self._qualifier_list.extend(
237 self.get_interactively_picked_testplans())
238 elif self.launcher.whitelist_selection:
239 self._qualifier_list.extend(self.get_default_testplans())
240 logger.info(_("Selected testplans: %r"), self._qualifier_list)
241
242 def get_interactively_picked_testplans(self):
243 """
244 Show an interactive dialog that allows the user to pick a list of
245 testplans. The set of testplans is limited to those offered by the
246 'default_providers' setting.
247
248 :returns:
249 A list of selected testplans
250 """
251 testplans = []
252 testplan_selection = []
253 for provider in self.provider_list:
254 testplans.extend(
255 [unit for unit in provider.unit_list if
256 unit.Meta.name == 'test plan' and
257 re.search(self.launcher.whitelist_filter, unit.partial_id)])
258 testplan_name_list = [testplan.tr_name() for testplan in testplans]
259 testplan_selection = [
260 testplans.index(t) for t in testplans if
261 re.search(self.launcher.whitelist_selection, t.partial_id)]
262 selected_list = self.display.run(
263 ShowMenu(_("Suite selection"), testplan_name_list,
264 testplan_selection))
265 if not selected_list:
266 raise SystemExit(_("No testplan selected, aborting"))
267 self._testplan_list.extend(
268 [testplans[selected_index] for selected_index in selected_list])
269 return [testplans[selected_index].get_qualifier() for selected_index
270 in selected_list]
271
272 def get_default_testplans(self):
273 testplans = []
274 for provider in self.provider_list:
275 testplans.extend([
276 unit.get_qualifier() for unit in provider.unit_list if
277 unit.Meta.name == 'test plan' and re.search(
278 self.launcher.whitelist_selection, unit.partial_id)])
279 return testplans
280
281 def create_transport(self):
282 """
283 Create the ISessionStateTransport based on the command line options
284
285 This sets the :ivar:`_transport`.
286 """
287 # TODO:
288 self._transport = None
289
290 @property
291 def expected_app_id(self):
292 return 'checkbox'
293
294 def select_local_jobs(self):
295 print(self.C.header(_("Selecting Job Generators")))
296 # Create a qualifier list that will pick all local jobs out of the
297 # subset of jobs also enumerated by the whitelists we've already
298 # picked.
299 #
300 # Since each whitelist is a qualifier that selects jobs enumerated
301 # within, we only need to and an exclusive qualifier that deselects
302 # non-local jobs and we're done.
303 qualifier_list = []
304 qualifier_list.extend(self._qualifier_list)
305 origin = Origin.get_caller_origin()
306 qualifier_list.append(FieldQualifier(
307 'plugin', OperatorMatcher(operator.ne, 'local'), origin,
308 inclusive=False))
309 local_job_list = select_jobs(
310 self.manager.state.job_list, qualifier_list)
311 self._update_desired_job_list(local_job_list)
312
313 def interactively_pick_jobs_to_run(self):
314 print(self.C.header(_("Selecting Jobs For Execution")))
315 self._update_desired_job_list(select_jobs(
316 self.manager.state.job_list, self._qualifier_list))
317 if self.launcher.skip_test_selection or not self.is_interactive:
318 return
319 tree = SelectableJobTreeNode.create_tree(
320 self.manager.state, self.manager.state.run_list)
321 title = _('Choose tests to run on your system:')
322 self.display.run(ScrollableTreeNode(tree, title))
323 # NOTE: tree.selection is correct but ordered badly. To retain
324 # the original ordering we should just treat it as a mask and
325 # use it to filter jobs from desired_job_list.
326 wanted_set = frozenset(tree.selection + tree.resource_jobs)
327 job_list = [job for job in self.manager.state.run_list
328 if job in wanted_set]
329 self._update_desired_job_list(job_list)
330
331 def export_and_send_results(self):
332 if self.is_interactive:
333 print(self.C.header(_("Results")))
334 exporter = self.manager.create_exporter(
335 '2013.com.canonical.plainbox::text')
336 exported_stream = io.BytesIO()
337 exporter.dump_from_session_manager(self.manager, exported_stream)
338 exported_stream.seek(0) # Need to rewind the file, puagh
339 # This requires a bit more finesse, as exporters output bytes
340 # and stdout needs a string.
341 translating_stream = ByteStringStreamTranslator(
342 sys.stdout, "utf-8")
343 copyfileobj(exported_stream, translating_stream)
344 # FIXME: this should probably not go to plainbox but checkbox-ng
345 base_dir = os.path.join(
346 os.getenv(
347 'XDG_DATA_HOME', os.path.expanduser("~/.local/share/")),
348 "plainbox")
349 if not os.path.exists(base_dir):
350 os.makedirs(base_dir)
351 exp_options = ['with-sys-info', 'with-summary', 'with-job-description',
352 'with-text-attachments', 'with-certification-status',
353 'with-job-defs', 'with-io-log', 'with-comments']
354 print()
355 if self.launcher.exporter is not Unset:
356 exporters = self.launcher.exporter
357 else:
358 exporters = [
359 '2013.com.canonical.plainbox::hexr',
360 '2013.com.canonical.plainbox::html',
361 '2013.com.canonical.plainbox::xlsx',
362 '2013.com.canonical.plainbox::json',
363 ]
364 for unit_name in exporters:
365 exporter = self.manager.create_exporter(
366 unit_name, exp_options, strict=False)
367 extension = exporter.unit.file_extension
368 results_path = os.path.join(
369 base_dir, 'submission.{}'.format(extension))
370 with open(results_path, "wb") as stream:
371 exporter.dump_from_session_manager(self.manager, stream)
372 print(_("View results") + " ({}): file://{}".format(
373 extension, results_path))
374 self.submission_file = os.path.join(base_dir, 'submission.xml')
375 if self.launcher.submit_to is not Unset:
376 if self.launcher.submit_to == 'certification':
377 # If we supplied a submit_url in the launcher, it
378 # should override the one in the config.
379 if self.launcher.submit_url:
380 self.config.c3_url = self.launcher.submit_url
381 # Same behavior for submit_to_hexr (a boolean flag which
382 # should result in adding "submit_to_hexr=1" to transport
383 # options later on)
384 if self.launcher.submit_to_hexr:
385 self.config.submit_to_hexr = True
386 # for secure_id, config (which is user-writable) should
387 # override launcher (which is not)
388 if not self.config.secure_id:
389 self.config.secure_id = self.launcher.secure_id
390 # Override the secure_id configuration with the one provided
391 # by the command-line option
392 if self.ns.secure_id:
393 self.config.secure_id = self.ns.secure_id
394 if self.config.secure_id is Unset:
395 again = True
396 if not self.is_interactive:
397 again = False
398 while again:
399 # TRANSLATORS: Do not translate the {} format marker.
400 if self.ask_for_confirmation(
401 _("\nSubmit results to {0}?".format(
402 self.launcher.submit_url))):
403 try:
404 self.config.secure_id = input(_("Secure ID: "))
405 except ValidationError:
406 print(
407 _("ERROR: Secure ID must be 15-character "
408 "(or more) alphanumeric string"))
409 else:
410 again = False
411 self.submit_certification_results()
412 else:
413 again = False
414 else:
415 # Automatically try to submit results if the secure_id is
416 # valid
417 self.submit_certification_results()
418
419 def submit_certification_results(self):
420 from checkbox_ng.certification import InvalidSecureIDError
421 transport_cls = get_all_transports().get('certification')
422 # TRANSLATORS: Do not translate the {} format markers.
423 print(_("Submitting results to {0} for secure_id {1}").format(
424 self.config.c3_url, self.config.secure_id))
425 option_chunks = []
426 option_chunks.append("secure_id={0}".format(self.config.secure_id))
427 if self.config.submit_to_hexr:
428 option_chunks.append("submit_to_hexr=1")
429 # Assemble the option string
430 options_string = ",".join(option_chunks)
431 # Create the transport object
432 try:
433 transport = transport_cls(
434 self.config.c3_url, options_string)
435 except InvalidSecureIDError as exc:
436 print(exc)
437 return False
438 with open(self.submission_file, "r", encoding='utf-8') as stream:
439 try:
440 # Send the data, reading from the fallback file
441 result = transport.send(stream, self.config)
442 if 'url' in result:
443 # TRANSLATORS: Do not translate the {} format marker.
444 print(_("Successfully sent, submission status"
445 " at {0}").format(result['url']))
446 else:
447 # TRANSLATORS: Do not translate the {} format marker.
448 print(_("Successfully sent, server response"
449 ": {0}").format(result))
450 except TransportError as exc:
451 print(str(exc))
452
453 def maybe_rerun_jobs(self):
454 # create a list of jobs that qualify for rerunning
455 rerun_candidates = []
456 for job in self.manager.state.run_list:
457 job_state = self.manager.state.job_state_map[job.id]
458 if job_state.result.outcome in (
459 IJobResult.OUTCOME_FAIL, IJobResult.OUTCOME_CRASH,
460 IJobResult.OUTCOME_NOT_SUPPORTED):
461 rerun_candidates.append(job)
462
463 # bail-out early if no job qualifies for rerunning
464 if not rerun_candidates:
465 return False
466 tree = SelectableJobTreeNode.create_tree(
467 self.manager.state, rerun_candidates)
468 # nothing to select in root node and categories - bailing out
469 if not tree.jobs and not tree._categories:
470 return False
471 # deselect all by default
472 tree.set_descendants_state(False)
473 self.display.run(ShowRerun(tree, _("Select jobs to re-run")))
474 wanted_set = frozenset(tree.selection)
475 if not wanted_set:
476 # nothing selected - nothing to run
477 return False
478 # include resource jobs that selected jobs depend on
479 resources_to_rerun = []
480 for job in wanted_set:
481 job_state = self.manager.state.job_state_map[job.id]
482 for inhibitor in job_state.readiness_inhibitor_list:
483 if inhibitor.cause == InhibitionCause.FAILED_DEP:
484 resources_to_rerun.append(inhibitor.related_job)
485 # reset outcome of jobs that are selected for re-running
486 for job in list(wanted_set) + resources_to_rerun:
487 from plainbox.impl.result import MemoryJobResult
488 self.manager.state.job_state_map[job.id].result = \
489 MemoryJobResult({})
490 self.run_all_selected_jobs()
491 return True
diff --git a/checkbox_ng/commands/sru.py b/checkbox_ng/commands/sru.py
492deleted file mode 1006440deleted file mode 100644
index 7db7b71..0000000
--- a/checkbox_ng/commands/sru.py
+++ /dev/null
@@ -1,174 +0,0 @@
1# This file is part of Checkbox.
2#
3#
4# Copyright 2013 Canonical Ltd.
5# Written by:
6# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
7#
8# Checkbox is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 3,
10# as published by the Free Software Foundation.
11
12#
13# Checkbox is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
20
21"""
22:mod:`checkbox_ng.commands.sru` -- sru sub-command
23==================================================
24
25.. warning::
26
27 THIS MODULE DOES NOT HAVE STABLE PUBLIC API
28"""
29import sys
30
31from gettext import gettext as _
32from plainbox.impl.commands import PlainBoxCommand
33from plainbox.impl.commands.inv_check_config import CheckConfigInvocation
34from plainbox.impl.ingredients import CanonicalCommand
35from plainbox.impl.secure.config import ValidationError, Unset
36
37
38class sru(CanonicalCommand):
39
40 """
41 Run stable release update (sru) tests.
42
43 Stable release updates are periodic fixes for nominated bugs that land in
44 existing supported Ubuntu releases. To ensure a certain level of quality
45 all SRU updates affecting hardware enablement are automatically tested
46 on a pool of certified machines.
47 """
48
49 def __init__(self, config):
50 """Init method to store the config settings."""
51 self.config = config
52 if not self.config.test_plan:
53 self.config.test_plan = "2013.com.canonical.certification::sru"
54
55 def register_arguments(self, parser):
56 """Method called to register command line arguments."""
57 parser.add_argument(
58 '--secure_id', metavar=_("SECURE-ID"),
59 # NOTE: --secure-id is optional only when set in a config file
60 required=self.config.secure_id is Unset,
61 help=_("Canonical hardware identifier"))
62 parser.add_argument(
63 '-T', '--test-plan',
64 action="store",
65 metavar=_("TEST-PLAN-ID"),
66 default=None,
67 # TRANSLATORS: this is in imperative form
68 help=_("load the specified test plan"))
69 parser.add_argument(
70 '--staging', action='store_true', default=False,
71 help=_("Send the data to non-production test server"))
72 parser.add_argument(
73 "--check-config",
74 action="store_true",
75 help=_("run check-config before starting"))
76
77 def invoked(self, ctx):
78 """Method called when the command is invoked."""
79 # Copy command-line arguments over configuration variables
80 try:
81 if ctx.args.secure_id:
82 self.config.secure_id = ctx.args.secure_id
83 if ctx.args.test_plan:
84 self.config.test_plan = ctx.args.test_plan
85 if ctx.args.staging:
86 self.config.staging = ctx.args.staging
87 except ValidationError as exc:
88 print(_("Configuration problems prevent running SRU tests"))
89 print(exc)
90 return 1
91 ctx.sa.use_alternate_configuration(self.config)
92 # Run check-config, if requested
93 if ctx.args.check_config:
94 retval = CheckConfigInvocation(lambda: self.config).run()
95 if retval != 0:
96 return retval
97 self.transport = self._create_transport(
98 ctx.sa, self.config.secure_id, self.config.staging)
99 self.ctx = ctx
100 try:
101 self._collect_info(ctx.rc, ctx.sa)
102 self._save_results(ctx.rc, ctx.sa)
103 self._send_results(
104 ctx.rc, ctx.sa, self.config.secure_id, self.config.staging)
105 except KeyboardInterrupt:
106 return 1
107
108 def _save_results(self, rc, sa):
109 rc.reset()
110 rc.padding = (1, 1, 0, 1)
111 path = sa.export_to_file(
112 "2013.com.canonical.plainbox::hexr", (), '/tmp')
113 rc.para(_("Results saved to {0}").format(path))
114
115 def _send_results(self, rc, sa, secure_id, staging):
116 rc.reset()
117 rc.padding = (1, 1, 0, 1)
118 rc.para(_("Sending hardware report to Canonical Certification"))
119 rc.para(_("Server URL is: {0}").format(self.transport.url))
120 result = sa.export_to_transport(
121 "2013.com.canonical.plainbox::hexr", self.transport)
122 if 'url' in result:
123 rc.para(result['url'])
124
125 def _create_transport(self, sa, secure_id, staging):
126 return sa.get_canonical_certification_transport(
127 secure_id, staging=staging)
128
129 def _collect_info(self, rc, sa):
130 sa.select_providers('*')
131 sa.start_new_session(_("SRU Session"))
132 sa.select_test_plan(self.config.test_plan)
133 sa.bootstrap()
134 for job_id in sa.get_static_todo_list():
135 job = sa.get_job(job_id)
136 builder = sa.run_job(job_id, 'silent', False)
137 result = builder.get_result()
138 sa.use_job_result(job_id, result)
139 rc.para("- {0}: {1}".format(job.id, result))
140 if result.comments:
141 rc.padding = (0, 0, 0, 2)
142 rc.para("{0}".format(result.comments))
143 rc.reset()
144
145
146class SRUCommand(PlainBoxCommand):
147
148 """
149 Command for running Stable Release Update (SRU) tests.
150
151 Stable release updates are periodic fixes for nominated bugs that land in
152 existing supported Ubuntu releases. To ensure a certain level of quality
153 all SRU updates affecting hardware enablement are automatically tested
154 on a pool of certified machines.
155 """
156
157 gettext_domain = "checkbox-ng"
158
159 def __init__(self, provider_loader, config_loader):
160 self.provider_loader = provider_loader
161 # This command does funky things to the command line parser and it
162 # needs to load the config subsystem *early* so let's just load it now.
163 self.config = config_loader()
164
165 def invoked(self, ns):
166 """Method called when the command is invoked."""
167 return sru(self.config).main(sys.argv[2:], exit=False)
168
169 def register_parser(self, subparsers):
170 """Method called to register command line arguments."""
171 parser = subparsers.add_parser(
172 "sru", help=_("run automated stable release update tests"))
173 parser.set_defaults(command=self)
174 sru(self.config).register_arguments(parser)
diff --git a/checkbox_ng/commands/submit.py b/checkbox_ng/commands/submit.py
175deleted file mode 1006440deleted file mode 100644
index 6d24a8a..0000000
--- a/checkbox_ng/commands/submit.py
+++ /dev/null
@@ -1,154 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2014 Canonical Ltd.
4# Written by:
5# Sylvain Pineau <sylvain.pineau@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10#
11# Checkbox is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
18
19"""
20:mod:`checkbox_ng.commands.submit` -- the submit sub-command
21============================================================
22
23.. warning::
24
25 THIS MODULE DOES NOT HAVE STABLE PUBLIC API
26"""
27
28from argparse import ArgumentTypeError
29from plainbox.i18n import docstring
30from plainbox.i18n import gettext as _
31from plainbox.i18n import gettext_noop as N_
32import re
33
34from plainbox.impl.commands import PlainBoxCommand
35from plainbox.impl.secure.config import Unset
36from plainbox.impl.transport import TransportError
37from plainbox.impl.transport import SECURE_ID_PATTERN
38
39from checkbox_ng.certification import CertificationTransport
40
41
42class SubmitInvocation:
43 """
44 Helper class instantiated to perform a particular invocation of the submit
45 command. Unlike the SRU command itself, this class is instantiated each
46 time.
47 """
48
49 def __init__(self, ns):
50 self.ns = ns
51
52 def run(self):
53 options_string = "secure_id={0}".format(self.ns.secure_id)
54 transport = CertificationTransport(self.ns.url, options_string)
55
56 try:
57 with open(self.ns.submission, "r", encoding='utf-8') as subm_file:
58 result = transport.send(subm_file)
59 except (TransportError, OSError) as exc:
60 raise SystemExit(exc)
61 else:
62 if 'url' in result:
63 # TRANSLATORS: Do not translate the {} format marker.
64 print(_("Successfully sent, submission status"
65 " at {0}").format(result['url']))
66 else:
67 # TRANSLATORS: Do not translate the {} format marker.
68 print(_("Successfully sent, server response"
69 ": {0}").format(result))
70
71
72@docstring(
73 # TRANSLATORS: please leave various options (both long and short forms),
74 # environment variables and paths in their original form. Also keep the
75 # special @EPILOG@ string. The first line of the translation is special and
76 # is used as the help message. Please keep the pseudo-statement form and
77 # don't finish the sentence with a dot. Pay extra attention to whitespace.
78 # It must be correctly preserved or the result won't work. In particular
79 # the leading whitespace *must* be preserved and *must* have the same
80 # length on each line.
81 N_("""
82 submit test results to the Canonical certification website
83
84 This command sends the XML results file to the Certification website.
85 """))
86class SubmitCommand(PlainBoxCommand):
87
88 gettext_domain = "checkbox-ng"
89
90 def __init__(self, config_loader):
91 self.config = config_loader()
92
93 def invoked(self, ns):
94 return SubmitInvocation(ns).run()
95
96 def register_parser(self, subparsers):
97 parser = subparsers.add_parser("submit", help=_(
98 "submit test results to the Canonical certification website"))
99 self.register_arguments(parser)
100
101 def register_arguments(self, parser):
102 parser.set_defaults(command=self)
103 parser.add_argument(
104 'submission', help=_("The path to the results xml file"))
105 self.register_optional_arguments(parser, required=True)
106
107 def register_optional_arguments(self, parser, required=False):
108 if self.config.secure_id is not Unset:
109 parser.set_defaults(secure_id=self.config.secure_id)
110
111 def secureid(secure_id):
112 if not re.match(SECURE_ID_PATTERN, secure_id):
113 raise ArgumentTypeError(
114 _("must be 15-character (or more) alphanumeric string"))
115 return secure_id
116
117 required_check = False
118 if required:
119 required_check = self.config.secure_id is Unset
120 parser.add_argument(
121 '--secure_id', metavar=_("SECURE-ID"),
122 required=required_check,
123 type=secureid,
124 help=_("associate submission with a machine using this SECURE-ID"))
125
126 # Interpret this setting here
127 # Please remember the Unset.__bool__() return False
128 # After Interpret the setting,
129 # self.config.submit_to_c3 should has value or be Unset.
130 try:
131 if (self.config.submit_to_c3 and
132 (self.config.submit_to_c3.lower() in ('yes', 'true') or
133 int(self.config.submit_to_c3) == 1)):
134 # self.config.c3_url has a default value written in config.py
135 parser.set_defaults(url=self.config.c3_url)
136 else:
137 # if submit_to_c3 is castable to int but not 1
138 # this is still set as Unset
139 # otherwise url requirement will be None
140 self.config.submit_to_c3 = Unset
141 except ValueError:
142 # When submit_to_c3 is something other than 'yes', 'true',
143 # castable to integer, it raises ValueError.
144 # e.g. 'no', 'false', 'asdf' ...etc.
145 # In this case, it is still set as Unset.
146 self.config.submit_to_c3 = Unset
147
148 required_check = False
149 if required:
150 required_check = self.config.submit_to_c3 is Unset
151 parser.add_argument(
152 '--url', metavar=_("URL"),
153 required=required_check,
154 help=_("destination to submit to"))
diff --git a/checkbox_ng/commands/test_sru.py b/checkbox_ng/commands/test_sru.py
155deleted file mode 1006440deleted file mode 100644
index 8d456ee..0000000
--- a/checkbox_ng/commands/test_sru.py
+++ /dev/null
@@ -1,56 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21plainbox.impl.commands.test_sru
22===============================
23
24Test definitions for plainbox.impl.box module
25"""
26
27from inspect import cleandoc
28from unittest import TestCase
29
30from plainbox.testing_utils.io import TestIO
31
32from checkbox_ng.main import main
33
34
35class TestSru(TestCase):
36
37 def test_help(self):
38 with TestIO(combined=True) as io:
39 with self.assertRaises(SystemExit) as call:
40 main(['sru', '--help'])
41 self.assertEqual(call.exception.args, (0,))
42 self.maxDiff = None
43 expected = """
44 usage: checkbox sru [-h] --secure_id SECURE-ID [-T TEST-PLAN-ID] [--staging]
45 [--check-config]
46
47 optional arguments:
48 -h, --help show this help message and exit
49 --secure_id SECURE-ID
50 Canonical hardware identifier
51 -T TEST-PLAN-ID, --test-plan TEST-PLAN-ID
52 load the specified test plan
53 --staging Send the data to non-production test server
54 --check-config run check-config before starting
55 """
56 self.assertEqual(io.combined, cleandoc(expected) + "\n")
diff --git a/checkbox_ng/config.py b/checkbox_ng/config.py
index 7107dc0..c84f2b8 100644
--- a/checkbox_ng/config.py
+++ b/checkbox_ng/config.py
@@ -22,13 +22,10 @@
22=====================================================22=====================================================
23"""23"""
2424
25from gettext import gettext as _
26import itertools25import itertools
27import os26import os
2827
29from plainbox.impl.applogic import PlainBoxConfig28from plainbox.impl.applogic import PlainBoxConfig
30from plainbox.impl.secure import config
31from plainbox.impl.transport import SECURE_ID_PATTERN
3229
3330
34class CheckBoxConfig(PlainBoxConfig):31class CheckBoxConfig(PlainBoxConfig):
@@ -36,44 +33,6 @@ class CheckBoxConfig(PlainBoxConfig):
36 Configuration for checkbox-ng33 Configuration for checkbox-ng
37 """34 """
3835
39 secure_id = config.Variable(
40 section="sru",
41 help_text=_("Secure ID of the system"),
42 validator_list=[config.PatternValidator(SECURE_ID_PATTERN)])
43
44 submit_to_c3 = config.Variable(
45 section="submission",
46 help_text=_("Whether to send the submission data to c3"))
47
48 submit_to_hexr = config.Variable(
49 section="submission",
50 help_text=_("Whether to also send the submission data to HEXR"),
51 kind=bool)
52
53 # TODO: Add a validator to check if URL looks fine
54 c3_url = config.Variable(
55 section="sru",
56 help_text=_("URL of the certification website"),
57 default="https://certification.canonical.com/submissions/submit/")
58
59 fallback_file = config.Variable(
60 section="sru",
61 help_text=_("Location of the fallback file"))
62
63 whitelist = config.Variable(
64 section="sru",
65 help_text=_("Optional whitelist with which to run SRU testing"))
66
67 test_plan = config.Variable(
68 section="sru",
69 help_text=_("Optional test plan with which to run SRU testing"))
70
71 staging = config.Variable(
72 section="sru",
73 kind=bool,
74 default=False,
75 help_text=_("Send the data to non-production test server"))
76
77 class Meta(PlainBoxConfig.Meta):36 class Meta(PlainBoxConfig.Meta):
78 # TODO: properly depend on xdg and use real code that also handles37 # TODO: properly depend on xdg and use real code that also handles
79 # XDG_CONFIG_HOME.38 # XDG_CONFIG_HOME.
diff --git a/checkbox_ng/launcher/checkbox_cli.py b/checkbox_ng/launcher/checkbox_cli.py
index 1317ed2..7c368c7 100644
--- a/checkbox_ng/launcher/checkbox_cli.py
+++ b/checkbox_ng/launcher/checkbox_cli.py
@@ -38,16 +38,15 @@ from plainbox.impl.ingredients import RenderingContextIngredient
38from plainbox.impl.ingredients import SessionAssistantIngredient38from plainbox.impl.ingredients import SessionAssistantIngredient
39from plainbox.impl.launcher import DefaultLauncherDefinition39from plainbox.impl.launcher import DefaultLauncherDefinition
40from plainbox.impl.launcher import LauncherDefinition40from plainbox.impl.launcher import LauncherDefinition
41from plainbox.vendor.textland import get_display
4241
43from checkbox_ng.launcher.subcommands import (42from checkbox_ng.launcher.subcommands import (
44 Launcher, List, Run, StartProvider, ListBootstrapped43 CheckConfig, Launcher, List, Run, StartProvider, Submit, ListBootstrapped
45)44)
4645
4746
48_ = gettext.gettext47_ = gettext.gettext
4948
50_logger = logging.getLogger("checkbox-launcher")49_logger = logging.getLogger("checkbox-cli")
5150
5251
53class DisplayIngredient(Ingredient):52class DisplayIngredient(Ingredient):
@@ -125,7 +124,6 @@ class CheckboxCommandRecipe(CommandRecipe):
125 LauncherIngredient(),124 LauncherIngredient(),
126 SessionAssistantIngredient(),125 SessionAssistantIngredient(),
127 RenderingContextIngredient(),126 RenderingContextIngredient(),
128 DisplayIngredient(),
129 ]127 ]
130128
131129
@@ -141,10 +139,12 @@ class CheckboxCommand(CanonicalCommand):
141 bug_report_url = "https://bugs.launchpad.net/checkbox-ng/+filebug"139 bug_report_url = "https://bugs.launchpad.net/checkbox-ng/+filebug"
142140
143 sub_commands = (141 sub_commands = (
142 ('check-config', CheckConfig),
144 ('launcher', Launcher),143 ('launcher', Launcher),
145 ('list', List),144 ('list', List),
146 ('run', Run),145 ('run', Run),
147 ('startprovider', StartProvider),146 ('startprovider', StartProvider),
147 ('submit', Submit),
148 ('list-bootstrapped', ListBootstrapped),148 ('list-bootstrapped', ListBootstrapped),
149 )149 )
150150
@@ -179,6 +179,7 @@ def main():
179 # $ checkbox-cli launcher my-launcher -> same as ^179 # $ checkbox-cli launcher my-launcher -> same as ^
180 # to achieve that the following code 'injects launcher subcommand to argv180 # to achieve that the following code 'injects launcher subcommand to argv
181 known_cmds = [x[0] for x in CheckboxCommand.sub_commands]181 known_cmds = [x[0] for x in CheckboxCommand.sub_commands]
182 known_cmds += ['-h', '--help']
182 if not (set(known_cmds) & set(sys.argv[1:])):183 if not (set(known_cmds) & set(sys.argv[1:])):
183 sys.argv.insert(1, 'launcher')184 sys.argv.insert(1, 'launcher')
184 CheckboxCommand().main()185 CheckboxCommand().main()
diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py
index 7f52cda..155824a 100644
--- a/checkbox_ng/launcher/subcommands.py
+++ b/checkbox_ng/launcher/subcommands.py
@@ -37,6 +37,7 @@ from guacamole import Command
37from plainbox.abc import IJobResult37from plainbox.abc import IJobResult
38from plainbox.i18n import ngettext38from plainbox.i18n import ngettext
39from plainbox.impl.color import Colorizer39from plainbox.impl.color import Colorizer
40from plainbox.impl.commands.inv_check_config import CheckConfigInvocation
40from plainbox.impl.commands.inv_run import Action41from plainbox.impl.commands.inv_run import Action
41from plainbox.impl.commands.inv_run import NormalUI42from plainbox.impl.commands.inv_run import NormalUI
42from plainbox.impl.commands.inv_startprovider import (43from plainbox.impl.commands.inv_startprovider import (
@@ -55,8 +56,10 @@ from plainbox.impl.session.restart import get_strategy_by_name
55from plainbox.impl.transport import TransportError56from plainbox.impl.transport import TransportError
56from plainbox.impl.transport import InvalidSecureIDError57from plainbox.impl.transport import InvalidSecureIDError
57from plainbox.impl.transport import get_all_transports58from plainbox.impl.transport import get_all_transports
59from plainbox.impl.transport import SECURE_ID_PATTERN
58from plainbox.public import get_providers60from plainbox.public import get_providers
5961
62from checkbox_ng.config import CheckBoxConfig
60from checkbox_ng.launcher.stages import MainLoopStage63from checkbox_ng.launcher.stages import MainLoopStage
61from checkbox_ng.urwid_ui import CategoryBrowser64from checkbox_ng.urwid_ui import CategoryBrowser
62from checkbox_ng.urwid_ui import ReRunBrowser65from checkbox_ng.urwid_ui import ReRunBrowser
@@ -67,6 +70,73 @@ _ = gettext.gettext
67_logger = logging.getLogger("checkbox-ng.launcher.subcommands")70_logger = logging.getLogger("checkbox-ng.launcher.subcommands")
6871
6972
73class CheckConfig(Command):
74 def invoked(self, ctx):
75 return CheckConfigInvocation(lambda: CheckBoxConfig.get()).run()
76
77
78class Submit(Command):
79 def register_arguments(self, parser):
80 def secureid(secure_id):
81 if not re.match(SECURE_ID_PATTERN, secure_id):
82 raise ArgumentTypeError(
83 _("must be 15-character (or more) alphanumeric string"))
84 return secure_id
85 parser.add_argument(
86 'secure_id', metavar=_("SECURE-ID"),
87 type=secureid,
88 help=_("associate submission with a machine using this SECURE-ID"))
89 parser.add_argument(
90 "submission", metavar=_("SUBMISSION"),
91 help=_("The path to the results file"))
92 parser.add_argument(
93 "-s", "--staging", action="store_true",
94 help=_("Use staging environment"))
95
96 def invoked(self, ctx):
97 transport_cls = None
98 enc = None
99 mode = 'rb'
100 options_string = "secure_id={0}".format(ctx.args.secure_id)
101 url = ('https://submission.canonical.com/'
102 'v1/submission/hardware/{}'.format(ctx.args.secure_id))
103 if ctx.args.staging:
104 url = ('https://submission.staging.canonical.com/'
105 'v1/submission/hardware/{}'.format(ctx.args.secure_id))
106 if ctx.args.submission.endswith('xml'):
107 from checkbox_ng.certification import CertificationTransport
108 transport_cls = CertificationTransport
109 mode = 'r'
110 enc = 'utf-8'
111 url = ('https://certification.canonical.com/'
112 'submissions/submit/')
113 if ctx.args.staging:
114 url = ('https://certification.staging.canonical.com/'
115 'submissions/submit/')
116 else:
117 from checkbox_ng.certification import SubmissionServiceTransport
118 transport_cls = SubmissionServiceTransport
119 transport = transport_cls(url, options_string)
120 try:
121 with open(ctx.args.submission, mode, encoding=enc) as subm_file:
122 result = transport.send(subm_file)
123 except (TransportError, OSError) as exc:
124 raise SystemExit(exc)
125 else:
126 if result and 'url' in result:
127 # TRANSLATORS: Do not translate the {} format marker.
128 print(_("Successfully sent, submission status"
129 " at {0}").format(result['url']))
130 elif result and 'status_url' in result:
131 # TRANSLATORS: Do not translate the {} format marker.
132 print(_("Successfully sent, submission status"
133 " at {0}").format(result['status_url']))
134 else:
135 # TRANSLATORS: Do not translate the {} format marker.
136 print(_("Successfully sent, server response"
137 ": {0}").format(result))
138
139
70class StartProvider(Command):140class StartProvider(Command):
71 def register_arguments(self, parser):141 def register_arguments(self, parser):
72 parser.add_argument(142 parser.add_argument(
@@ -114,10 +184,6 @@ class Launcher(Command, MainLoopStage):
114 print(_("Launcher seems valid."))184 print(_("Launcher seems valid."))
115 return185 return
116 self.launcher = ctx.cmd_toplevel.launcher186 self.launcher = ctx.cmd_toplevel.launcher
117 if not self.launcher.launcher_version:
118 # it's a legacy launcher, use legacy way of running commands
119 from checkbox_ng.tools import CheckboxLauncherTool
120 raise SystemExit(CheckboxLauncherTool().main(sys.argv[1:]))
121 logging_level = {187 logging_level = {
122 'normal': logging.WARNING,188 'normal': logging.WARNING,
123 'verbose': logging.INFO,189 'verbose': logging.INFO,
@@ -897,6 +963,7 @@ class List(Command):
897 print(_("--format applies only to 'all-jobs' group. Ignoring..."))963 print(_("--format applies only to 'all-jobs' group. Ignoring..."))
898 print_objs(ctx.args.GROUP, ctx.args.attrs)964 print_objs(ctx.args.GROUP, ctx.args.attrs)
899965
966
900class ListBootstrapped(Command):967class ListBootstrapped(Command):
901 name = 'list-bootstrapped'968 name = 'list-bootstrapped'
902969
diff --git a/checkbox_ng/main.py b/checkbox_ng/main.py
903deleted file mode 100644970deleted file mode 100644
index 178edd9..0000000
--- a/checkbox_ng/main.py
+++ /dev/null
@@ -1,61 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2012-2014 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10#
11# Checkbox is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
18
19"""
20:mod:`checkbox_ng.main` -- entry points for command line tools
21==============================================================
22"""
23
24import logging
25
26from plainbox.impl.logging import setup_logging
27
28from checkbox_ng.tools import CheckboxLauncherTool
29from checkbox_ng.tools import CheckboxSubmitTool
30from checkbox_ng.tools import CheckboxTool
31
32
33logger = logging.getLogger("checkbox.ng.main")
34
35
36def main(argv=None):
37 """
38 checkbox command line utility
39 """
40 raise SystemExit(CheckboxTool().main(argv))
41
42
43def submit(argv=None):
44 """
45 checkbox-submit command line utility
46 """
47 raise SystemExit(CheckboxSubmitTool().main(argv))
48
49
50def launcher(argv=None):
51 """
52 checkbox-launcher command line utility
53 """
54 raise SystemExit(CheckboxLauncherTool().main(argv))
55
56
57# Setup logging before anything else starts working.
58# If we do it in main() or some other place then unit tests will see
59# "leaked" log files which are really closed when the runtime shuts
60# down but not when the tests are finishing
61setup_logging()
diff --git a/checkbox_ng/misc.py b/checkbox_ng/misc.py
62deleted file mode 1006440deleted file mode 100644
index e5547cd..0000000
--- a/checkbox_ng/misc.py
+++ /dev/null
@@ -1,476 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013-2014 Canonical Ltd.
4# Written by:
5# Sylvain Pineau <sylvain.pineau@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10#
11# Checkbox is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
18
19"""
20:mod:`checkbox_ng.misc` -- Other stuff
21======================================
22
23.. warning::
24
25 THIS MODULE DOES NOT HAVE STABLE PUBLIC API
26"""
27
28from gettext import gettext as _
29from logging import getLogger
30
31from plainbox.abc import IJobResult
32
33
34logger = getLogger("checkbox.ng.commands.cli")
35
36
37class JobTreeNode:
38
39 r"""
40 JobTreeNode class is used to store a tree structure.
41
42 A tree consists of a collection of JobTreeNode instances connected in a
43 hierarchical way where nodes are used as categories, jobs belonging to a
44 category are listed in the node leaves.
45
46 Example::
47 / Job A
48 Root-|
49 | / Job B
50 \--- Category X |
51 \ Job C
52 """
53
54 def __init__(self, name=None):
55 """ Initialize the job tree node with a given name. """
56 self._name = name if name else 'Root'
57 self._parent = None
58 self._categories = []
59 self._jobs = []
60
61 @property
62 def name(self):
63 """ name of this node. """
64 return self._name
65
66 @property
67 def parent(self):
68 """ parent node for this node. """
69 return self._parent
70
71 @property
72 def categories(self):
73 """ list of sub categories. """
74 return self._categories
75
76 @property
77 def jobs(self):
78 """ job(s) belonging to this node/category. """
79 return self._jobs
80
81 @property
82 def depth(self):
83 """ level of depth for this node. """
84 return (self._parent.depth + 1) if self._parent else 0
85
86 def __str__(self):
87 """ same as self.name. """
88 return self.name
89
90 def __repr__(self):
91 """ Get a representation of this node for debugging. """
92 return "<JobTreeNode name:{!r}>".format(self.name)
93
94 def add_category(self, category):
95 """
96 Add a new category to this node.
97
98 :param category:
99 The node instance to be added as a category.
100 """
101 self._categories.append(category)
102 # Always keep this list sorted to easily find a given child by index
103 self._categories.sort(key=lambda item: item.name)
104 category._parent = self
105
106 def add_job(self, job):
107 """
108 Add a new job to this node.
109
110 :param job:
111 The job instance to be added to this node.
112 """
113 self._jobs.append(job)
114 # Always keep this list sorted to easily find a given leaf by index
115 # Note bisect.insort(a, x) cannot be used here as JobDefinition are
116 # not sortable
117 self._jobs.sort(key=lambda item: item.id)
118
119 def get_ancestors(self):
120 """ Get the list of ancestors from here to the root of the tree. """
121 ancestors = []
122 node = self
123 while node.parent is not None:
124 ancestors.append(node.parent)
125 node = node.parent
126 return ancestors
127
128 def get_descendants(self):
129 """ Return a list of all descendant category nodes. """
130 descendants = []
131 for category in self.categories:
132 descendants.append(category)
133 descendants.extend(category.get_descendants())
134 return descendants
135
136 @classmethod
137 def create_tree(cls, session_state, job_list):
138 """
139 Build a rooted JobTreeNode from a job list.
140
141 :argument session_state:
142 A session state object
143 :argument job_list:
144 List of jobs to consider for building the tree.
145 """
146 builder = TreeBuilder(session_state, cls)
147 for job in job_list:
148 builder.auto_add_job(job)
149 return builder.root_node
150
151 @classmethod
152 def create_simple_tree(cls, sa, job_list):
153 """
154 Build a rooted JobTreeNode from a job list.
155
156 :argument sa:
157 A session assistant object
158 :argument job_list:
159 List of jobs to consider for building the tree.
160 """
161 root_node = cls()
162 for job in job_list:
163 cat_id = sa.get_job_state(job.id).effective_category_id
164 cat_name = sa.get_category(cat_id).tr_name()
165 matches = [n for n in root_node.categories if n.name == cat_name]
166 if not matches:
167 node = cls(cat_name)
168 root_node.add_category(node)
169 else:
170 node = matches[0]
171 node.add_job(job)
172 return root_node
173
174 @classmethod
175 def create_rerun_tree(cls, sa, job_list):
176 """
177 Build a rooted JobTreeNode from a job list for the re-run screen.
178 The jobs are categorized by their outcome (failed, skipped, ...)
179 instead of by the category they belong to.
180
181 :argument sa:
182 A session assistant object
183 :argument job_list:
184 List of jobs to consider for building the tree.
185 """
186 section_names = {
187 IJobResult.OUTCOME_FAIL: _("Failed Jobs"),
188 IJobResult.OUTCOME_SKIP: _("Skipped Jobs"),
189 IJobResult.OUTCOME_CRASH: _("Crashed Jobs"),
190 }
191 root_node = cls()
192 for job in job_list:
193 cat_id = sa.get_job_state(job.id).effective_category_id
194 cat_name = sa.get_category(cat_id).tr_name()
195 job_outcome = sa.get_job_state(job.id).result.outcome
196 job_section = section_names[job_outcome]
197 matches = [n for n in root_node.categories if n.name == job_section]
198 if not matches:
199 node = cls(job_section)
200 root_node.add_category(node)
201 else:
202 node = matches[0]
203
204 node.add_job(job)
205 return root_node
206
207
208class TreeBuilder:
209
210 """
211 Builder for :class:`JobTreeNode`.
212
213
214 Helper class that assists in building a tree of :class:`JobTreeNode`
215 objects out of job definitions and their associations, as expressed by
216 :attr:`JobState.via_job` associated with each job.
217
218 The builder is a single-use object and should be re-created for each new
219 construct. Internally it stores the job_state_map of the
220 :class:`SessionState` it was created with as well as additional helper
221 state.
222 """
223
224 def __init__(self, session_state: "SessionState", node_cls):
225 self._job_state_map = session_state.job_state_map
226 self._node_cls = node_cls
227 self._root_node = node_cls()
228 self._category_node_map = {} # id -> node
229
230 @property
231 def root_node(self):
232 return self._root_node
233
234 def auto_add_job(self, job):
235 """
236 Add a job to the tree, automatically creating category nodes as needed.
237
238 :param job:
239 The job definition to add.
240 """
241 if job.plugin == 'local':
242 # For local jobs, just create the category node but don't add the
243 # local job itself there.
244 self.get_or_create_category_node(job)
245 else:
246 # For all other jobs, look at the parent job (if any) and create
247 # the category node out of that node. This never fails as "None" is
248 # the root_node object.
249 state = self._job_state_map[job.id]
250 node = self.get_or_create_category_node(state.via_job)
251 # Then add that job to the category node
252 node.add_job(job)
253
254 def get_or_create_category_node(self, category_job):
255 """
256 Get a category node for a given job.
257
258 Get or create a :class:`JobTreeNode` that corresponds to the
259 category defined (somehow) by the job ``category_job``.
260
261 :param category_job:
262 The job that describes the category. This is either a
263 plugin="local" job or a plugin="resource" job. This can also be
264 None, which is a shorthand to say "root node".
265 :returns:
266 The ``root_node`` if ``category_job`` is None. A freshly
267 created node, created with :func:`create_category_node()` if
268 the category_job was never seen before (as recorded by the
269 category_node_map).
270 """
271 logger.debug("get_or_create_category_node(%r)", category_job)
272 if category_job is None:
273 return self._root_node
274 if category_job.id not in self._category_node_map:
275 category_node = self.create_category_node(category_job)
276 # The category is added to its parent, that's either the root
277 # (if we're standalone) or the non-root category this one
278 # belongs to.
279 category_state = self._job_state_map[category_job.id]
280 if category_state.via_job is not None:
281 parent_category_node = self.get_or_create_category_node(
282 category_state.via_job)
283 else:
284 parent_category_node = self._root_node
285 parent_category_node.add_category(category_node)
286 else:
287 category_node = self._category_node_map[category_job.id]
288 return category_node
289
290 def create_category_node(self, category_job):
291 """
292 Create a category node for a given job.
293
294 Create a :class:`JobTreeNode` that corresponds to the category defined
295 (somehow) by the job ``category_job``.
296
297 :param category_job:
298 The job that describes the node to create.
299 :returns:
300 A fresh node with appropriate data.
301 """
302 logger.debug("create_category_node(%r)", category_job)
303 if category_job.summary == category_job.partial_id:
304 category_node = self._node_cls(category_job.description)
305 else:
306 category_node = self._node_cls(category_job.summary)
307 self._category_node_map[category_job.id] = category_node
308 return category_node
309
310
311class SelectableJobTreeNode(JobTreeNode):
312 """
313 Implementation of a node in a tree that can be selected/deselected
314 """
315 def __init__(self, job=None):
316 super().__init__(job)
317 self.selected = True
318 self.job_selection = {}
319 self.expanded = True
320 self.current_index = 0
321 self._resource_jobs = []
322
323 def __len__(self):
324 l = 0
325 if self.expanded:
326 for category in self.categories:
327 l += 1 + len(category)
328 for job in self.jobs:
329 l += 1
330 return l
331
332 def get_node_by_index(self, index, tree=None):
333 """
334 Return the node found at the position given by index considering the
335 tree from a top-down list view.
336 """
337 if tree is None:
338 tree = self
339 if self.expanded:
340 for category in self.categories:
341 if index == tree.current_index:
342 tree.current_index = 0
343 return (category, None)
344 else:
345 tree.current_index += 1
346 result = category.get_node_by_index(index, tree)
347 if result[0] is not None and result[1] is not None:
348 return result
349 for job in self.jobs:
350 if index == tree.current_index:
351 tree.current_index = 0
352 return (job, self)
353 else:
354 tree.current_index += 1
355 return (None, None)
356
357 def render(self, cols=80, as_summary=True):
358 """
359 Return the tree as a simple list of categories and jobs suitable for
360 display. Jobs are properly indented to respect the tree hierarchy
361 and selection marks are added automatically at the beginning of each
362 element.
363
364 The node titles should not exceed the width of a the terminal and
365 thus are cut to fit inside.
366
367 :param cols:
368 The number of columns to render.
369 :param as_summary:
370 Whether we display the job summaries or their partial IDs.
371 """
372 self._flat_list = []
373 if self.expanded:
374 for category in self.categories:
375 prefix = '[ ]'
376 if category.selected:
377 prefix = '[X]'
378 line = ''
379 title = category.name
380 if category.jobs or category.categories:
381 if category.expanded:
382 line = prefix + self.depth * ' ' + ' - ' + title
383 else:
384 line = prefix + self.depth * ' ' + ' + ' + title
385 else:
386 line = prefix + self.depth * ' ' + ' ' + title
387 if len(line) > cols:
388 col_max = cols - 4 # includes len('...') + a space
389 line = line[:col_max] + '...'
390 self._flat_list.append(line)
391 self._flat_list.extend(category.render(cols, as_summary))
392 for job in self.jobs:
393 prefix = '[ ]'
394 if self.job_selection[job]:
395 prefix = '[X]'
396 if as_summary:
397 title = job.tr_summary()
398 else:
399 title = job.partial_id
400 line = prefix + self.depth * ' ' + ' ' + title
401 if len(line) > cols:
402 col_max = cols - 4 # includes len('...') + a space
403 line = line[:col_max] + '...'
404 self._flat_list.append(line)
405 return self._flat_list
406
407 def add_job(self, job):
408 if job.plugin == 'resource':
409 # I don't want the user to see resources but I need to keep
410 # track of them to put them in the final selection. I also
411 # don't want to add them to the tree.
412 self._resource_jobs.append(job)
413 return
414 super().add_job(job)
415 self.job_selection[job] = True
416
417 @property
418 def selection(self):
419 """
420 Return all the jobs currently selected
421 """
422 self._selection_list = []
423 for category in self.categories:
424 self._selection_list.extend(category.selection)
425 for job in self.job_selection:
426 if self.job_selection[job]:
427 self._selection_list.append(job)
428 return self._selection_list
429
430 @property
431 def resource_jobs(self):
432 """Return all the resource jobs."""
433 return self._resource_jobs
434
435 def set_ancestors_state(self, new_state):
436 """
437 Set the selection state of all ancestors consistently
438 """
439 # If child is set, then all ancestors must be set
440 if new_state:
441 parent = self.parent
442 while parent:
443 parent.selected = new_state
444 parent = parent.parent
445 # If child is not set, then all ancestors mustn't be set
446 # unless another child of the ancestor is set
447 else:
448 parent = self.parent
449 while parent:
450 if any((category.selected
451 for category in parent.categories)):
452 break
453 if any((parent.job_selection[job]
454 for job in parent.job_selection)):
455 break
456 parent.selected = new_state
457 parent = parent.parent
458
459 def update_selected_state(self):
460 """
461 Update the category state according to its job selection
462 """
463 if any((self.job_selection[job] for job in self.job_selection)):
464 self.selected = True
465 else:
466 self.selected = False
467
468 def set_descendants_state(self, new_state):
469 """
470 Set the selection state of all descendants recursively
471 """
472 self.selected = new_state
473 for job in self.job_selection:
474 self.job_selection[job] = new_state
475 for category in self.categories:
476 category.set_descendants_state(new_state)
diff --git a/checkbox_ng/test_config.py b/checkbox_ng/test_config.py
477deleted file mode 1006440deleted file mode 100644
index 33e3a24..0000000
--- a/checkbox_ng/test_config.py
+++ /dev/null
@@ -1,46 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21checkbox_ng.test_config
22=======================
23
24Test definitions for checkbox_ng.config module
25"""
26
27from unittest import TestCase
28
29from plainbox.impl.secure.config import Unset
30
31from checkbox_ng.config import CheckBoxConfig
32
33
34class PlainBoxConfigTests(TestCase):
35
36 def test_smoke(self):
37 config = CheckBoxConfig()
38 self.assertIs(config.secure_id, Unset)
39 secure_id = "0123456789ABCDE"
40 config.secure_id = secure_id
41 self.assertEqual(config.secure_id, secure_id)
42 with self.assertRaises(ValueError):
43 config.secure_id = "bork"
44 self.assertEqual(config.secure_id, secure_id)
45 del config.secure_id
46 self.assertIs(config.secure_id, Unset)
diff --git a/checkbox_ng/test_main.py b/checkbox_ng/test_main.py
47deleted file mode 1006440deleted file mode 100644
index 1d2981d..0000000
--- a/checkbox_ng/test_main.py
+++ /dev/null
@@ -1,93 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2012-2014 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21checkbox_ng.test_main
22=====================
23
24Test definitions for checkbox_ng.main module
25"""
26
27from inspect import cleandoc
28from unittest import TestCase
29
30from plainbox.impl.clitools import ToolBase
31from plainbox.testing_utils.io import TestIO
32
33from checkbox_ng import __version__ as version
34from checkbox_ng.main import main
35
36
37class TestMain(TestCase):
38
39 def test_version(self):
40 with TestIO(combined=True) as io:
41 with self.assertRaises(SystemExit) as call:
42 main(['--version'])
43 self.assertEqual(call.exception.args, (0,))
44 self.assertEqual(io.combined, "{}\n".format(version))
45
46 def test_help(self):
47 with TestIO(combined=True) as io:
48 with self.assertRaises(SystemExit) as call:
49 main(['--help'])
50 self.assertEqual(call.exception.args, (0,))
51 self.maxDiff = None
52 expected = """
53 usage: checkbox [-h] [--version] [-v] [-D] [-C] [-T LOGGER] [-P] [-I]
54 {sru,check-config,submit,launcher,self-test} ...
55
56 positional arguments:
57 {sru,check-config,submit,launcher,self-test}
58 sru run automated stable release update tests
59 check-config check and display plainbox configuration
60 submit submit test results to the Canonical certification
61 website
62 launcher run a customized testing session
63 self-test run unit and integration tests
64
65 optional arguments:
66 -h, --help show this help message and exit
67 --version show program's version number and exit
68
69 logging and debugging:
70 -v, --verbose be more verbose (same as --log-level=INFO)
71 -D, --debug enable DEBUG messages on the root logger
72 -C, --debug-console display DEBUG messages in the console
73 -T LOGGER, --trace LOGGER
74 enable DEBUG messages on the specified logger (can be
75 used multiple times)
76 -P, --pdb jump into pdb (python debugger) when a command crashes
77 -I, --debug-interrupt
78 crash on SIGINT/KeyboardInterrupt, useful with --pdb
79 """
80 self.assertEqual(io.combined, cleandoc(expected) + "\n")
81
82 def test_run_without_args(self):
83 with TestIO(combined=True) as io:
84 with self.assertRaises(SystemExit) as call:
85 main([])
86 self.assertEqual(call.exception.args, (2,))
87 expected = """
88 usage: checkbox [-h] [--version] [-v] [-D] [-C] [-T LOGGER] [-P] [-I]
89 {sru,check-config,submit,launcher,self-test} ...
90 checkbox: error: too few arguments
91
92 """
93 self.assertEqual(io.combined, cleandoc(expected) + "\n")
diff --git a/checkbox_ng/test_misc.py b/checkbox_ng/test_misc.py
94deleted file mode 1006440deleted file mode 100644
index efb26ac..0000000
--- a/checkbox_ng/test_misc.py
+++ /dev/null
@@ -1,183 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2014 Canonical Ltd.
4# Written by:
5# Sylvain Pineau <sylvain.pineau@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21checkbox_ng.commands.test_cli
22=============================
23
24Test definitions for checkbox_ng.commands.cli module
25"""
26
27from unittest import TestCase
28
29from plainbox.impl.session import SessionState
30from plainbox.impl.testing_utils import make_job
31from plainbox.impl.unit.job import JobDefinition
32
33from checkbox_ng.misc import JobTreeNode
34from checkbox_ng.misc import SelectableJobTreeNode
35
36
37class TestJobTreeNode(TestCase):
38
39 def setUp(self):
40 A = make_job('A')
41 B = make_job('B', plugin='local', description='foo')
42 C = make_job('C')
43 D = make_job('D', plugin='shell')
44 E = make_job('E', plugin='local', description='bar')
45 F = make_job('F', plugin='shell')
46 G = make_job('G', plugin='local', description='baz')
47 R = make_job('R', plugin='resource')
48 Z = make_job('Z', plugin='local', description='zaz')
49 state = SessionState([A, B, C, D, E, F, G, R, Z])
50 # D and E are a child of B
51 state.job_state_map[D.id].via_job = B
52 state.job_state_map[E.id].via_job = B
53 # F is a child of E
54 state.job_state_map[F.id].via_job = E
55 self.tree = JobTreeNode.create_tree(
56 state, [R, B, C, D, E, F, G, A, Z])
57
58 def test_create_tree(self):
59 self.assertIsInstance(self.tree, JobTreeNode)
60 self.assertEqual(len(self.tree.categories), 3)
61 [self.assertIsInstance(c, JobTreeNode) for c in self.tree.categories]
62 self.assertEqual(len(self.tree.jobs), 3)
63 [self.assertIsInstance(j, JobDefinition) for j in self.tree.jobs]
64 self.assertIsNone(self.tree.parent)
65 self.assertEqual(self.tree.depth, 0)
66 node = self.tree.categories[1]
67 self.assertEqual(node.name, 'foo')
68 self.assertEqual(len(node.categories), 1)
69 [self.assertIsInstance(c, JobTreeNode) for c in node.categories]
70 self.assertEqual(len(node.jobs), 1)
71 [self.assertIsInstance(j, JobDefinition) for j in node.jobs]
72
73
74class TestSelectableJobTreeNode(TestCase):
75
76 def setUp(self):
77 self.A = make_job('a', name='A')
78 self.B = make_job('b', name='B', plugin='local', description='foo')
79 self.C = make_job('c', name='C')
80 self.D = make_job('d', name='D', plugin='shell')
81 self.E = make_job('e', name='E', plugin='shell')
82 self.F = make_job('f', name='F', plugin='resource', description='baz')
83 state = SessionState([self.A, self.B, self.C, self.D, self.E, self.F])
84 # D and E are a child of B
85 state.job_state_map[self.D.id].via_job = self.B
86 state.job_state_map[self.E.id].via_job = self.B
87 self.tree = SelectableJobTreeNode.create_tree(state, [
88 self.A,
89 self.B,
90 self.C,
91 self.D,
92 self.E,
93 self.F
94 ])
95
96 def test_create_tree(self):
97 self.assertIsInstance(self.tree, SelectableJobTreeNode)
98 self.assertEqual(len(self.tree.categories), 1)
99 [self.assertIsInstance(c, SelectableJobTreeNode)
100 for c in self.tree.categories]
101 self.assertEqual(len(self.tree.jobs), 2)
102 [self.assertIsInstance(j, JobDefinition) for j in self.tree.jobs]
103 self.assertTrue(self.tree.selected)
104 [self.assertTrue(self.tree.job_selection[j])
105 for j in self.tree.job_selection]
106 self.assertTrue(self.tree.expanded)
107 self.assertIsNone(self.tree.parent)
108 self.assertEqual(self.tree.depth, 0)
109
110 def test_get_node_by_index(self):
111 self.assertEqual(self.tree.get_node_by_index(0)[0].name, 'foo')
112 self.assertEqual(self.tree.get_node_by_index(1)[0].name, 'D')
113 self.assertEqual(self.tree.get_node_by_index(2)[0].name, 'E')
114 self.assertEqual(self.tree.get_node_by_index(3)[0].name, 'A')
115 self.assertEqual(self.tree.get_node_by_index(4)[0].name, 'C')
116 self.assertIsNone(self.tree.get_node_by_index(5)[0])
117
118 def test_render(self):
119 expected = ['[X] - foo',
120 '[X] d',
121 '[X] e',
122 '[X] a',
123 '[X] c']
124 self.assertEqual(self.tree.render(), expected)
125
126 def test_render_deselected_all(self):
127 self.tree.set_descendants_state(False)
128 expected = ['[ ] - foo',
129 '[ ] d',
130 '[ ] e',
131 '[ ] a',
132 '[ ] c']
133 self.assertEqual(self.tree.render(), expected)
134
135 def test_render_reselected_all(self):
136 self.tree.set_descendants_state(False)
137 self.tree.set_descendants_state(True)
138 expected = ['[X] - foo',
139 '[X] d',
140 '[X] e',
141 '[X] a',
142 '[X] c']
143 self.assertEqual(self.tree.render(), expected)
144
145 def test_render_with_child_collapsed(self):
146 self.tree.categories[0].expanded = False
147 expected = ['[X] + foo',
148 '[X] a',
149 '[X] c']
150 self.assertEqual(self.tree.render(), expected)
151
152 def test_set_ancestors_state(self):
153 self.tree.set_descendants_state(False)
154 node = self.tree.categories[0]
155 node.job_selection[self.E] = True
156 node.update_selected_state()
157 node.set_ancestors_state(node.selected)
158 expected = ['[X] - foo',
159 '[ ] d',
160 '[X] e',
161 '[ ] a',
162 '[ ] c']
163 self.assertEqual(self.tree.render(), expected)
164 node.selected = not(node.selected)
165 node.set_ancestors_state(node.selected)
166 node.set_descendants_state(node.selected)
167 expected = ['[ ] - foo',
168 '[ ] d',
169 '[ ] e',
170 '[ ] a',
171 '[ ] c']
172 self.assertEqual(self.tree.render(), expected)
173
174 def test_selection(self):
175 self.tree.set_descendants_state(False)
176 node = self.tree.categories[0]
177 node.job_selection[self.D] = True
178 node.update_selected_state()
179 node.set_ancestors_state(node.selected)
180 # Note that in addition to the selected (D) test, we need the
181 # tree selection to contain the resource (F), even though the
182 # user never saw it in the previous tests for visual presentation.
183 self.assertEqual(self.tree.selection, [self.D])
diff --git a/checkbox_ng/tools.py b/checkbox_ng/tools.py
184deleted file mode 1006440deleted file mode 100644
index 1aa8f37..0000000
--- a/checkbox_ng/tools.py
+++ /dev/null
@@ -1,146 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2012-2015 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10#
11# Checkbox is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
18
19"""
20:mod:`checkbox_ng.tools` -- top-level command line tools
21========================================================
22"""
23
24import logging
25import os
26
27from plainbox.impl.clitools import SingleCommandToolMixIn
28from plainbox.impl.clitools import ToolBase
29from plainbox.impl.commands.cmd_selftest import SelfTestCommand
30from plainbox.public import get_providers
31
32from checkbox_ng import __version__ as version
33from checkbox_ng.config import CheckBoxConfig
34from checkbox_ng.tests import load_unit_tests
35
36
37logger = logging.getLogger("checkbox.ng.tools")
38
39
40class CheckboxToolBase(ToolBase):
41 """
42 Base class for all checkbox-ng tools.
43
44 This class contains some shared code like configuration, providers, i18n
45 and version handling.
46 """
47
48 def _load_config(self):
49 return self.get_config_cls().get()
50
51 def _load_providers(self):
52 return get_providers()
53
54 @classmethod
55 def get_exec_version(cls):
56 """
57 Get the version of the checkbox-ng package
58 """
59 return version
60
61 @classmethod
62 def get_config_cls(cls):
63 """
64 Get particular sub-class of the Config class to use
65 """
66 return CheckBoxConfig
67
68 def get_gettext_domain(self):
69 """
70 Get the 'checkbox-ng' gettext domain
71 """
72 return "checkbox-ng"
73
74 def get_locale_dir(self):
75 """
76 Get an optional development locale directory specific to checkbox-ng
77 """
78 return os.getenv("CHECKBOX_NG_LOCALE_DIR", None)
79
80
81class CheckboxTool(CheckboxToolBase):
82 """
83 Tool that implements the new checkbox command.
84
85 This tool has two sub-commands:
86
87 checkbox sru - to run stable release update testing
88 checkbox check-config - to validate and display system configuration
89 """
90
91 @classmethod
92 def get_exec_name(cls):
93 return "checkbox"
94
95 def add_subcommands(self, subparsers, early_ns=None):
96 from checkbox_ng.commands.launcher import LauncherCommand
97 from checkbox_ng.commands.sru import SRUCommand
98 from checkbox_ng.commands.submit import SubmitCommand
99 from plainbox.impl.commands.cmd_check_config import CheckConfigCommand
100 SRUCommand(
101 self._load_providers, self._load_config
102 ).register_parser(subparsers)
103 CheckConfigCommand(
104 self._load_config
105 ).register_parser(subparsers)
106 SubmitCommand(
107 self._load_config
108 ).register_parser(subparsers)
109 LauncherCommand(
110 self._load_providers, self._load_config
111 ).register_parser(subparsers)
112 SelfTestCommand(load_unit_tests).register_parser(subparsers)
113
114
115class CheckboxSubmitTool(SingleCommandToolMixIn, CheckboxToolBase):
116 """
117 A tool class that implements checkbox-submit.
118
119 This tool implements the submit feature to send test results to the
120 Canonical certification website
121 """
122
123 @classmethod
124 def get_exec_name(cls):
125 return "checkbox-submit"
126
127 def get_command(self):
128 from checkbox_ng.commands.submit import SubmitCommand
129 return SubmitCommand(self._load_config)
130
131
132class CheckboxLauncherTool(SingleCommandToolMixIn, CheckboxToolBase):
133 """
134 A tool class that implements checkbox-launcher.
135
136 This tool implements configurable text-mode-graphics launchers that perform
137 a pre-defined testing session based on the launcher profile.
138 """
139
140 @classmethod
141 def get_exec_name(cls):
142 return "checkbox-launcher"
143
144 def get_command(self):
145 from checkbox_ng.commands.launcher import LauncherCommand
146 return LauncherCommand(self._load_providers, self._load_config)
diff --git a/checkbox_ng/ui.py b/checkbox_ng/ui.py
147deleted file mode 1006440deleted file mode 100644
index 322c522..0000000
--- a/checkbox_ng/ui.py
+++ /dev/null
@@ -1,363 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013-2015 Canonical Ltd.
4# Written by:
5# Sylvain Pineau <sylvain.pineau@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21:mod:`checkbox_ng.ui` -- user interface elements
22================================================
23"""
24
25from gettext import gettext as _
26from logging import getLogger
27import textwrap
28
29from plainbox.vendor.textland import DrawingContext
30from plainbox.vendor.textland import EVENT_KEYBOARD
31from plainbox.vendor.textland import EVENT_RESIZE
32from plainbox.vendor.textland import Event
33from plainbox.vendor.textland import IApplication
34from plainbox.vendor.textland import Size
35from plainbox.vendor.textland import TextImage
36from plainbox.vendor.textland import NORMAL, REVERSE
37
38
39logger = getLogger("checkbox.ng.ui")
40
41
42class ShowWelcome(IApplication):
43 """
44 Display a welcome message
45 """
46 def __init__(self, text):
47 self.image = TextImage(Size(0, 0))
48 self.text = text
49
50 def consume_event(self, event: Event):
51 if event.kind == EVENT_RESIZE:
52 self.image = TextImage(event.data) # data is the new size
53 elif event.kind == EVENT_KEYBOARD and event.data.key == "enter":
54 raise StopIteration
55 self.repaint(event)
56 return self.image
57
58 def repaint(self, event: Event):
59 ctx = DrawingContext(self.image)
60 i = 0
61 ctx.border()
62 for paragraph in self.text.splitlines():
63 i += 1
64 for line in textwrap.fill(
65 paragraph,
66 self.image.size.width - 8,
67 replace_whitespace=False).splitlines():
68 ctx.move_to(4, i)
69 ctx.print(line)
70 i += 1
71 ctx.move_to(4, i + 1)
72 ctx.attributes.style = REVERSE
73 ctx.print(_("< Continue >"))
74
75
76class ShowMenu(IApplication):
77 """
78 Display the appropriate menu and return the selected options
79 """
80 def __init__(self, title, menu, selection=[0], multiple_allowed=True):
81 self.image = TextImage(Size(0, 0))
82 self.title = title
83 self.menu = menu
84 self.option_count = len(menu)
85 self.position = 0 # Zero-based index of the selected menu option
86 self.multiple_allowed = multiple_allowed
87 if self.option_count:
88 self.selection = selection
89 else:
90 self.selection = []
91
92 def consume_event(self, event: Event):
93 if event.kind == EVENT_RESIZE:
94 self.image = TextImage(event.data) # data is the new size
95 elif event.kind == EVENT_KEYBOARD:
96 if event.data.key == "down":
97 if self.position < self.option_count:
98 self.position += 1
99 else:
100 self.position = 0
101 elif event.data.key == "up":
102 if self.position > 0:
103 self.position -= 1
104 else:
105 self.position = self.option_count
106 elif (event.data.key == "enter" and
107 self.position == self.option_count):
108 raise StopIteration(self.selection)
109 elif event.data.key == "space":
110 if self.position in self.selection:
111 self.selection.remove(self.position)
112 elif self.position < self.option_count:
113 self.selection.append(self.position)
114 if not self.multiple_allowed:
115 self.selection = [self.position]
116 self.repaint(event)
117 return self.image
118
119 def repaint(self, event: Event):
120 ctx = DrawingContext(self.image)
121 ctx.border(tm=1)
122 ctx.attributes.style = REVERSE
123 ctx.print(' ' * self.image.size.width)
124 ctx.move_to(1, 0)
125 ctx.print(self.title)
126
127 # Display all the menu items
128 for i in range(self.option_count):
129 ctx.attributes.style = NORMAL
130 if i == self.position:
131 ctx.attributes.style = REVERSE
132 # Display options from line 3, column 4
133 ctx.move_to(4, 3 + i)
134 ctx.print("[{}] - {}".format(
135 'X' if i in self.selection else ' ',
136 self.menu[i].replace('ihv-', '')))
137
138 # Display "OK" at bottom of menu
139 ctx.attributes.style = NORMAL
140 if self.position == self.option_count:
141 ctx.attributes.style = REVERSE
142 # Add an empty line before the last option
143 ctx.move_to(4, 4 + self.option_count)
144 ctx.print("< OK >")
145
146
147class ScrollableTreeNode(IApplication):
148 """
149 Class used to interact with a SelectableJobTreeNode
150 """
151 def __init__(self, tree, title):
152 self.image = TextImage(Size(0, 0))
153 self.tree = tree
154 self.title = title
155 self.top = 0 # Top line number
156 self.highlight = 0 # Highlighted line number
157 self.summary = True
158
159 def consume_event(self, event: Event):
160 if event.kind == EVENT_RESIZE:
161 self.image = TextImage(event.data) # data is the new size
162 elif event.kind == EVENT_KEYBOARD:
163 self.image = TextImage(self.image.size)
164 if event.data.key == "up":
165 self._scroll("up")
166 elif event.data.key == "down":
167 self._scroll("down")
168 elif event.data.key == "space":
169 self._selectNode()
170 elif event.data.key == "enter":
171 self._toggleNode()
172 elif event.data.key in 'sS':
173 self.tree.set_descendants_state(True)
174 elif event.data.key in 'dD':
175 self.tree.set_descendants_state(False)
176 elif event.data.key in 'iI':
177 self.summary = not self.summary
178 elif event.data.key in 'tT':
179 raise StopIteration
180 self.repaint(event)
181 return self.image
182
183 def repaint(self, event: Event):
184 ctx = DrawingContext(self.image)
185 ctx.border(tm=1, bm=1)
186 cols = self.image.size.width
187 extra_cols = 0
188 if cols > 80:
189 extra_cols = cols - 80
190 ctx.attributes.style = REVERSE
191 ctx.print(' ' * cols)
192 ctx.move_to(1, 0)
193 bottom = self.top + self.image.size.height - 4
194 ctx.print(self.title)
195 ctx.move_to(1, self.image.size.height - 1)
196 ctx.attributes.style = REVERSE
197 ctx.print(_("Enter"))
198 ctx.move_to(6, self.image.size.height - 1)
199 ctx.attributes.style = NORMAL
200 ctx.print(_(": Expand/Collapse"))
201 ctx.move_to(27, self.image.size.height - 1)
202 ctx.attributes.style = REVERSE
203 # FIXME: i18n problem
204 ctx.print("S")
205 ctx.move_to(28, self.image.size.height - 1)
206 ctx.attributes.style = NORMAL
207 ctx.print("elect All")
208 ctx.move_to(41, self.image.size.height - 1)
209 ctx.attributes.style = REVERSE
210 # FIXME: i18n problem
211 ctx.print("D")
212 ctx.move_to(42, self.image.size.height - 1)
213 ctx.attributes.style = NORMAL
214 ctx.print("eselect All")
215 ctx.move_to(66 + extra_cols, self.image.size.height - 1)
216 ctx.print(_("Start "))
217 ctx.move_to(72 + extra_cols, self.image.size.height - 1)
218 ctx.attributes.style = REVERSE
219 # FIXME: i18n problem
220 ctx.print("T")
221 ctx.move_to(73 + extra_cols, self.image.size.height - 1)
222 ctx.attributes.style = NORMAL
223 ctx.print("esting")
224 for i, line in enumerate(self.tree.render(cols - 3,
225 as_summary=self.summary)[self.top:bottom]):
226 ctx.move_to(2, i + 2)
227 if i != self.highlight:
228 ctx.attributes.style = NORMAL
229 else: # highlight the current line
230 ctx.attributes.style = REVERSE
231 ctx.print(line)
232
233 def _selectNode(self):
234 """
235 Mark a node/job as selected for this test run.
236 See :meth:`SelectableJobTreeNode.set_ancestors_state()` and
237 :meth:`SelectableJobTreeNode.set_descendants_state()` for details
238 about the automatic selection of parents and descendants.
239 """
240 node, category = self.tree.get_node_by_index(self.top + self.highlight)
241 if category: # then the selected node is a job not a category
242 job = node
243 category.job_selection[job] = not(category.job_selection[job])
244 category.update_selected_state()
245 category.set_ancestors_state(category.job_selection[job])
246 else:
247 node.selected = not(node.selected)
248 node.set_descendants_state(node.selected)
249 node.set_ancestors_state(node.selected)
250
251 def _toggleNode(self):
252 """
253 Expand/collapse a node
254 """
255 node, is_job = self.tree.get_node_by_index(self.top + self.highlight)
256 if node is not None and not is_job:
257 node.expanded = not(node.expanded)
258
259 def _scroll(self, direction):
260 visible_length = len(self.tree)
261 # Scroll the tree view
262 if (direction == "up" and
263 self.highlight == 0 and self.top != 0):
264 self.top -= 1
265 return
266 elif (direction == "down" and
267 (self.highlight + 1) == (self.image.size.height - 4) and
268 (self.top + self.image.size.height - 4) != visible_length):
269 self.top += 1
270 return
271 # Move the highlighted line
272 if (direction == "up" and
273 (self.top != 0 or self.highlight != 0)):
274 self.highlight -= 1
275 elif (direction == "down" and
276 (self.top + self.highlight + 1) != visible_length and
277 (self.highlight + 1) != (self.image.size.height - 4)):
278 self.highlight += 1
279
280
281class ShowRerun(ScrollableTreeNode):
282 """ Display the re-run screen."""
283 def __init__(self, tree, title):
284 super().__init__(tree, title)
285
286 def consume_event(self, event: Event):
287 if event.kind == EVENT_RESIZE:
288 self.image = TextImage(event.data) # data is the new size
289 elif event.kind == EVENT_KEYBOARD:
290 self.image = TextImage(self.image.size)
291 if event.data.key == "up":
292 self._scroll("up")
293 elif event.data.key == "down":
294 self._scroll("down")
295 elif event.data.key == "space":
296 self._selectNode()
297 elif event.data.key == "enter":
298 self._toggleNode()
299 elif event.data.key in 'sS':
300 self.tree.set_descendants_state(True)
301 elif event.data.key in 'dD':
302 self.tree.set_descendants_state(False)
303 elif event.data.key in 'fF':
304 self.tree.set_descendants_state(False)
305 raise StopIteration
306 elif event.data.key in 'rR':
307 raise StopIteration
308 self.repaint(event)
309 return self.image
310
311 def repaint(self, event: Event):
312 ctx = DrawingContext(self.image)
313 ctx.border(tm=1, bm=1)
314 cols = self.image.size.width
315 extra_cols = 0
316 if cols > 80:
317 extra_cols = cols - 80
318 ctx.attributes.style = REVERSE
319 ctx.print(' ' * cols)
320 ctx.move_to(1, 0)
321 bottom = self.top + self.image.size.height - 4
322 ctx.print(self.title)
323 ctx.move_to(1, self.image.size.height - 1)
324 ctx.attributes.style = REVERSE
325 ctx.print(_("Enter"))
326 ctx.move_to(6, self.image.size.height - 1)
327 ctx.attributes.style = NORMAL
328 ctx.print(_(": Expand/Collapse"))
329 ctx.move_to(27, self.image.size.height - 1)
330 ctx.attributes.style = REVERSE
331 # FIXME: i18n problem
332 ctx.print("S")
333 ctx.move_to(28, self.image.size.height - 1)
334 ctx.attributes.style = NORMAL
335 ctx.print("elect All")
336 ctx.move_to(41, self.image.size.height - 1)
337 ctx.attributes.style = REVERSE
338 # FIXME: i18n problem
339 ctx.print("D")
340 ctx.move_to(42, self.image.size.height - 1)
341 ctx.attributes.style = NORMAL
342 ctx.print("eselect All")
343 ctx.move_to(63 + extra_cols, self.image.size.height - 1)
344 ctx.attributes.style = REVERSE
345 # FIXME: i18n problem
346 ctx.print("F")
347 ctx.move_to(64 + extra_cols, self.image.size.height - 1)
348 ctx.attributes.style = NORMAL
349 ctx.print(_("inish"))
350 ctx.move_to(73 + extra_cols, self.image.size.height - 1)
351 ctx.attributes.style = REVERSE
352 # FIXME: i18n problem
353 ctx.print("R")
354 ctx.move_to(74 + extra_cols, self.image.size.height - 1)
355 ctx.attributes.style = NORMAL
356 ctx.print("e-run")
357 for i, line in enumerate(self.tree.render(cols - 3)[self.top:bottom]):
358 ctx.move_to(2, i + 2)
359 if i != self.highlight:
360 ctx.attributes.style = NORMAL
361 else: # highlight the current line
362 ctx.attributes.style = REVERSE
363 ctx.print(line)
diff --git a/po/POTFILES.in b/po/POTFILES.in
index c9804d5..8934fc2 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,19 +1,13 @@
1[encoding: UTF-8]1[encoding: UTF-8]
2./docs/conf.py
2./checkbox_ng/__init__.py3./checkbox_ng/__init__.py
3./checkbox_ng/certification.py4./checkbox_ng/certification.py
4./checkbox_ng/commands/__init__.py
5./checkbox_ng/commands/cli.py
6./checkbox_ng/commands/launcher.py
7./checkbox_ng/commands/newcli.py
8./checkbox_ng/commands/sru.py
9./checkbox_ng/commands/submit.py
10./checkbox_ng/commands/test_sru.py
11./checkbox_ng/config.py5./checkbox_ng/config.py
12./checkbox_ng/launchpad.py6./checkbox_ng/launcher/__init__.py
13./checkbox_ng/main.py7./checkbox_ng/launcher/checkbox_cli.py
8./checkbox_ng/launcher/stages.py
9./checkbox_ng/launcher/subcommands.py
14./checkbox_ng/test_certification.py10./checkbox_ng/test_certification.py
15./checkbox_ng/test_config.py
16./checkbox_ng/test_main.py
17./checkbox_ng/test_misc.py
18./checkbox_ng/tests.py11./checkbox_ng/tests.py
19./checkbox_ng/ui.py12./checkbox_ng/urwid_ui.py
13./setup.py
diff --git a/setup.py b/setup.py
index b6226bf..f4ddbf5 100755
--- a/setup.py
+++ b/setup.py
@@ -68,9 +68,6 @@ setup(
68 entry_points={68 entry_points={
69 'console_scripts': [69 'console_scripts': [
70 'checkbox-cli=checkbox_ng.launcher.checkbox_cli:main',70 'checkbox-cli=checkbox_ng.launcher.checkbox_cli:main',
71 'checkbox=checkbox_ng.main:main',
72 'checkbox-submit=checkbox_ng.main:submit',
73 'checkbox-launcher=checkbox_ng.main:launcher',
74 ],71 ],
75 'plainbox.transport': [72 'plainbox.transport': [
76 'certification='73 'certification='

Subscribers

People subscribed via source and target branches