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

Subscribers

People subscribed via source and target branches