def test_custom(self): """ Ensure that .custom(_) works and obeys color settings """ self.assertEqual( Colorizer(False).custom("<text>", "<ansi-code>"), "<text>") self.assertEqual( Colorizer(True).custom("<text>", "<ansi-code>"), "<ansi-code><text>\x1b[0m")
def run_job(self, job, job_state, environ=None, ui=None): logger.info(_("Running %r"), job) if job.plugin not in supported_plugins: print(Colorizer().RED("Unsupported plugin type: {}".format( job.plugin))) return JobResultBuilder( outcome=IJobResult.OUTCOME_SKIP, comments=_("Unsupported plugin type: {}".format(job.plugin)) ).get_result() # resource and attachment jobs are always run (even in dry runs) if self._dry_run and job.plugin not in ('resource', 'attachment'): return JobResultBuilder( outcome=IJobResult.OUTCOME_SKIP, comments=_("Job skipped in dry-run mode") ).get_result() self._job_runner_ui_delegate.ui = ui # for cached resource jobs we get the result using cache # if it's not in the cache, ordinary "_run_command" will be run if job.plugin == 'resource' and 'cachable' in job.get_flag_set(): from_cache, result = self._resource_cache.get( job.checksum, lambda: self._run_command( job, environ).get_result()) if from_cache: print(Colorizer().header(_("Using cached data!"))) jrud = self._job_runner_ui_delegate jrud.on_begin('', dict()) for io_log_entry in result.io_log: jrud.on_chunk(io_log_entry.stream_name, io_log_entry.data) jrud.on_end(result.return_code) return result # manual jobs don't require running anything so we just return # the 'undecided' outcome if job.plugin == 'manual': return JobResultBuilder( outcome=IJobResult.OUTCOME_UNDECIDED).get_result() # all other kinds of jobs at this point need to run their command if not job.command: print(Colorizer().RED("No command to run!")) return JobResultBuilder( outcome=IJobResult.OUTCOME_FAIL, comments=_("No command to run!") ).get_result() result_builder = self._run_command(job, environ) # for user-interact-verify and user-verify jobs the operator chooses # the final outcome, so we need to reset the outcome to undecided # (from what command's return code would have set) if job.plugin in ('user-interact-verify', 'user-verify'): result_builder.outcome = IJobResult.OUTCOME_UNDECIDED # by this point the result_builder should have all the info needed # to yield appropriate result return result_builder.get_result()
def __init__(self, provider_loader, config_loader, ns, color): super().__init__(provider_loader, config_loader) self.ns = ns self._manager = None self._runner = None self._exporter = None self._transport = None self._backtrack_and_run_missing = True self._color = color self._test_plan = self.find_test_plan() self.C = Colorizer(color)
def __init__(self, action_list, prompt=None, color=None): """ :param action_list: A list of 3-tuples (accel, label, cmd) :prompt: An optional prompt string :returns: cmd of the selected action or None """ if prompt is None: prompt = _("Pick an action") self.action_list = action_list self.prompt = prompt self.C = Colorizer(color)
def invoked(self, ctx): self._C = Colorizer() self._override_exporting(self.local_export) self._launcher_text = '' self._is_bootstrapping = False self._target_host = ctx.args.host self._normal_user = '' self.launcher = DefaultLauncherDefinition() if ctx.args.launcher: expanded_path = os.path.expanduser(ctx.args.launcher) if not os.path.exists(expanded_path): raise SystemExit( _("{} launcher file was not found!").format(expanded_path)) with open(expanded_path, 'rt') as f: self._launcher_text = f.read() self.launcher.read_string(self._launcher_text) if ctx.args.user: self._normal_user = ctx.args.user timeout = 600 deadline = time.time() + timeout port = ctx.args.port if not ipaddress.ip_address(ctx.args.host).is_loopback: print( _("Connecting to {}:{}. Timeout: {}s").format( ctx.args.host, port, timeout)) while time.time() < deadline: try: self.connect_and_run(ctx.args.host, port) break except (ConnectionRefusedError, socket.timeout, OSError): print('.', end='', flush=True) time.sleep(1) else: print(_("\nConnection timed out."))
def invoked(self, ctx): if ctx.args.version: from checkbox_ng.version import get_version_info for component, version in get_version_info().items(): print("{}: {}".format(component, version)) return if ctx.args.verify: # validation is always run, so if there were any errors the program # exited by now, so validation passed print(_("Launcher seems valid.")) return if ctx.args.launcher: self.launcher = load_configs(ctx.args.launcher) else: self.launcher = DefaultLauncherDefinition() logging_level = { 'normal': logging.WARNING, 'verbose': logging.INFO, 'debug': logging.DEBUG, }[self.launcher.verbosity] if not ctx.args.verbose and not ctx.args.debug: # Command line args take precendence logging.basicConfig(level=logging_level) try: self._C = Colorizer() self.ctx = ctx # now we have all the correct flags and options, so we need to # replace the previously built SA with the defaults self._configure_restart(ctx) self._prepare_transports() ctx.sa.use_alternate_configuration(self.launcher) if not self._maybe_resume_session(): self._start_new_session() self._pick_jobs_to_run() if not self.ctx.sa.get_static_todo_list(): return 0 if 'submission_files' in self.launcher.stock_reports: print("Reports will be saved to: {}".format(self.base_dir)) # we initialize the nb of attempts for all the selected jobs... for job_id in self.ctx.sa.get_dynamic_todo_list(): job_state = self.ctx.sa.get_job_state(job_id) job_state.attempts = self.launcher.max_attempts # ... before running them self._run_jobs(self.ctx.sa.get_dynamic_todo_list()) if self.is_interactive and not self.launcher.auto_retry: while True: if not self._maybe_rerun_jobs(): break elif self.launcher.auto_retry: while True: if not self._maybe_auto_rerun_jobs(): break self._export_results() ctx.sa.finalize_session() return 0 if ctx.sa.get_summary()['fail'] == 0 else 1 except KeyboardInterrupt: return 1
class TextSessionStateExporter(SessionStateExporterBase): """Human-readable session state exporter.""" def __init__(self, option_list=None, color=None, exporter_unit=None): super().__init__(option_list, exporter_unit=exporter_unit) self.C = Colorizer(color) def get_session_data_subset(self, session_manager): return session_manager.state return self._trim_session_manager(session_manager).state def dump(self, session, stream): for job in session.run_list: state = session.job_state_map[job.id] if state.result.is_hollow: continue if self.C.is_enabled: stream.write(" {}: {}\n".format( self.C.custom( outcome_meta(state.result.outcome).unicode_sigil, outcome_meta(state.result.outcome).color_ansi), state.job.tr_summary(), ).encode("UTF-8")) if len(state.result_history) > 1: stream.write( _(" history: {0}\n").format(', '.join( self.C.custom(result.outcome_meta().tr_outcome, result.outcome_meta().color_ansi) for result in state.result_history)).encode("UTF-8")) else: stream.write("{:^15}: {}\n".format( state.result.tr_outcome(), state.job.tr_summary(), ).encode("UTF-8")) if state.result_history: print( _("History:"), ', '.join( self.C.custom(result.outcome_meta().unicode_sigil, result.outcome_meta().color_ansi) for result in state.result_history).encode("UTF-8"))
class ActionUI: """ A simple user interface to display a list of actions and let the user to pick one """ def __init__(self, action_list, prompt=None, color=None): """ :param action_list: A list of 3-tuples (accel, label, cmd) :prompt: An optional prompt string :returns: cmd of the selected action or None """ if prompt is None: prompt = _("Pick an action") self.action_list = action_list self.prompt = prompt self.C = Colorizer(color) def run(self): long_hint = "\n".join(" {accel} => {label}".format( accel=self.C.BLUE(action.accel) if action.accel else ' ', label=action.label) for action in self.action_list) short_hint = ''.join(action.accel for action in self.action_list) while True: try: print(self.C.BLUE(self.prompt)) print(long_hint) choice = input("[{}]: ".format(self.C.BLUE(short_hint))) except EOFError: return None else: for action in self.action_list: if choice == action.accel or choice == action.label: return action.cmd
class SimpleUI(NormalUI, MainLoopStage): """ Simplified version of the NormalUI from checkbox_ng.launcher.run. The simplification is mainly about just dealing with text that is to be displayed, instead of the plainbox abstractions like job, job state, etc. It's a class just for namespacing purposes. """ C = Colorizer() # XXX: evaluate other ways of aggregating those functions def description(header, text): print(SimpleUI.C.WHITE(header)) print() print(SimpleUI.C.CYAN(text)) print() def header(header): print(SimpleUI.C.header(header, fill='-')) def green_text(text, end='\n'): print(SimpleUI.C.GREEN(text), end=end, file=sys.stdout) def red_text(text, end='\n'): print(SimpleUI.C.RED(text), end=end, file=sys.stderr) def black_text(text, end='\n'): print(SimpleUI.C.BLACK(text), end=end, file=sys.stdout) def horiz_line(): print(SimpleUI.C.WHITE('-' * 80)) @property def is_interactive(self): return True @property def sa(self): None
def invoked(self, ctx): try: self._C = Colorizer() self.ctx = ctx self._configure_restart() config = load_configs() self.sa.use_alternate_configuration(config) self.sa.start_new_session(self.ctx.args.title or 'checkbox-run', UnifiedRunner) tps = self.sa.get_test_plans() self._configure_report() selection = ctx.args.PATTERN submission_message = self.ctx.args.message if len(selection) == 1 and selection[0] in tps: self.ctx.sa.update_app_blob( json.dumps({ 'testplan_id': selection[0], 'description': submission_message }).encode("UTF-8")) self.just_run_test_plan(selection[0]) else: self.ctx.sa.update_app_blob( json.dumps({ 'description': submission_message }).encode("UTF-8")) self.sa.hand_pick_jobs(selection) print(self.C.header(_("Running Selected Jobs"))) self._run_jobs(self.sa.get_dynamic_todo_list()) # there might have been new jobs instantiated while True: self.sa.hand_pick_jobs(ctx.args.PATTERN) todos = self.sa.get_dynamic_todo_list() if not todos: break self._run_jobs(self.sa.get_dynamic_todo_list()) self.sa.finalize_session() self._print_results() return 0 if self.sa.get_summary()['fail'] == 0 else 1 except KeyboardInterrupt: return 1
class RunInvocation(CheckBoxInvocationMixIn): """ Invocation of the 'plainbox run' command. attr ns: The argparse namespace obtained from RunCommand attr _manager: The SessionManager object attr _runner: The JobRunner object attr _exporter: A ISessionStateExporter of some kind attr _transport: A ISessionStateTransport of some kind (optional) attr _backtrack_and_run_missing: A flag indicating that we should run over all the jobs in the self.state.run_list again, set every time a job is added. Reset every time the loop-over-all-jobs is started. """ def __init__(self, provider_loader, config_loader, ns, color): super().__init__(provider_loader, config_loader) self.ns = ns self._manager = None self._runner = None self._exporter = None self._transport = None self._backtrack_and_run_missing = True self._color = color self._test_plan = self.find_test_plan() self.C = Colorizer(color) @property def manager(self): """ SessionManager object of the current session """ return self._manager @property def runner(self): """ JobRunner object of the current session """ return self._runner @property def state(self): """ SessionState object of the current session """ return self.manager.state @property def metadata(self): """ SessionMetaData object of the current session """ return self.state.metadata @property def storage(self): """ SessionStorage object of the current session """ return self.manager.storage @property def exporter(self): """ The ISessionStateExporter of the current session """ return self._exporter @property def transport(self): """ The ISessionStateTransport of the current session (optional) """ return self._transport @property def is_interactive(self): """ Flag indicating that this is an interactive invocation and we can interact with the user when we encounter OUTCOME_UNDECIDED """ return (sys.stdin.isatty() and sys.stdout.isatty() and not self.ns.non_interactive) def run(self): ns = self.ns if ns.transport == _('?'): self._print_transport_list(ns) return 0 else: return self.do_normal_sequence() def do_normal_sequence(self): """ Proceed through normal set of steps that are required to runs jobs """ # Create transport early so that we can handle bugs before starting the # session. self.create_transport() if self.is_interactive: resumed = self.maybe_resume_session() else: self.create_manager(None) resumed = False if self.ns.output_options == _('?'): self._print_output_option_list(self.ns) return 0 elif self.ns.output_format == _('?'): self._print_output_format_list(self.ns) return 0 if self.ns.output_format not in self.manager.exporter_map: print(_("invalid choice: '{}'".format(self.ns.output_format))) self._print_output_format_list(self.ns) return 1 # Create exporter after we get a session to query the manager and get # all exporter units self.create_exporter() # Create the job runner so that we can do stuff self.create_runner() # Set the effective category for each job self.set_effective_categories() # If we haven't resumed then do some one-time initialization if not resumed: # Store the application-identifying meta-data and checkpoint the # session. self.store_application_metadata() self.metadata.flags.add(SessionMetaData.FLAG_INCOMPLETE) self.manager.checkpoint() # Select all the jobs that we are likely to run. This is the # initial selection as we haven't started any jobs yet. self.do_initial_job_selection() # Print out our estimates self.print_estimated_duration() # Maybe ask the secure launcher to prompt for the password now. self.maybe_warm_up_authentication() # Iterate through the run list and run jobs if possible. This function # also implements backtrack to run new jobs that were added (and # selected) at runtime. When it exits all the jobs on the run list have # a result. self.run_all_selected_jobs() self.metadata.flags.remove(SessionMetaData.FLAG_INCOMPLETE) self.manager.checkpoint() # Export the result of the session and pass it to the transport to # finish the test run. self.export_and_send_results() self.metadata.flags.add(SessionMetaData.FLAG_SUBMITTED) self.manager.checkpoint() # FIXME: sensible return value return 0 def maybe_resume_session(self): # Try to use the first session that can be resumed if the user agrees resume_storage_list = self.get_resume_candidates() resume_storage = None resumed = False if resume_storage_list: print(self.C.header(_("Resume Incomplete Session"))) print( ngettext( "There is {0} incomplete session that might be resumed", "There are {0} incomplete sessions that might be resumed", len(resume_storage_list)).format(len(resume_storage_list))) for resume_storage in resume_storage_list: # Skip sessions that the user doesn't want to resume cmd = self._pick_action_cmd( [ Action('r', _("resume this session"), 'resume'), Action('n', _("next session"), 'next'), Action('c', _("create new session"), 'create') ], _("Do you want to resume session {0!a}?").format( resume_storage.id)) if cmd == 'resume': pass elif cmd == 'next': continue elif cmd == 'create' or cmd is None: self.create_manager(None) break # Skip sessions that cannot be resumed try: self.create_manager(resume_storage) except SessionResumeError: cmd = self._pick_action_cmd([ Action('i', _("ignore this problem"), 'ignore'), Action('e', _("erase this session"), 'erase') ]) if cmd == 'erase': resume_storage.remove() print(_("Session removed")) continue else: resumed = True # If we resumed maybe not rerun the same, probably broken job if resume_storage is not None: self.handle_last_job_after_resume() # Finally ignore other sessions that can be resumed break else: if resume_storage is not None and not self.ask_for_new_session(): # TRANSLATORS: This is the exit message raise SystemExit(_("Session not resumed")) # Create a fresh session if nothing got resumed self.create_manager(None) return resumed def _print_output_format_list(self, ns): print(_("Available output formats:")) for id, exporter in self.manager.exporter_map.items(): print("{} - {}".format(id, exporter.summary)) def _print_output_option_list(self, ns): print(_("Each format may support a different set of options")) for name, exporter in self.manager.exporter_map.items(): print("{}: {}".format( name, ", ".join(exporter.exporter_cls.supported_option_list))) def _print_transport_list(self, ns): print( _("Available transports: {}").format(', '.join( get_all_transports()))) def get_resume_candidates(self): """ Look at all of the suspended sessions and pick a list of candidates that could be used to resume the session now. """ storage_list = [] for storage in SessionStorageRepository().get_storage_list(): data = storage.load_checkpoint() if len(data) == 0: continue try: metadata = SessionPeekHelper().peek(data) except SessionResumeError as exc: logger.warning(_("Corrupted session %s: %s"), storage.id, exc) else: if (metadata.app_id == self.expected_app_id and metadata.title == self.expected_session_title and SessionMetaData.FLAG_INCOMPLETE in metadata.flags): storage_list.append(storage) return storage_list def ask_for_confirmation(self, message): return self._pick_action_cmd( [Action('y', _("yes"), True), Action('n', _("no"), False)], message) def ask_for_new_session(self): return self.ask_for_confirmation( _("Do you want to start a new session?")) def handle_last_job_after_resume(self): last_job = self.metadata.running_job_name if last_job is None: return print( _("Previous session run tried to execute job: {}").format( last_job)) cmd = self._pick_action_cmd([ Action('s', _("skip that job"), 'skip'), Action('p', _("mark it as passed and continue"), 'pass'), Action('f', _("mark it as failed and continue"), 'fail'), Action('r', _("run it again"), 'run'), ], _("What do you want to do with that job?")) if cmd == 'skip' or cmd is None: result = MemoryJobResult({ 'outcome': IJobResult.OUTCOME_SKIP, 'comments': _("Skipped after resuming execution") }) elif cmd == 'pass': result = MemoryJobResult({ 'outcome': IJobResult.OUTCOME_PASS, 'comments': _("Passed after resuming execution") }) elif cmd == 'fail': result = MemoryJobResult({ 'outcome': IJobResult.OUTCOME_FAIL, 'comments': _("Failed after resuming execution") }) elif cmd == 'run': result = None if result: self.state.update_job_result( self.state.job_state_map[last_job].job, result) self.metadata.running_job_name = None self.manager.checkpoint() def create_exporter(self): """ Create the ISessionStateExporter based on the command line options This sets the attr:`_exporter`. """ if self.ns.output_options: option_list = self.ns.output_options.split(',') else: option_list = None self._exporter = self.manager.create_exporter(self.ns.output_format, option_list) def create_transport(self): """ Create the ISessionStateTransport based on the command line options This sets the attr:`_transport`. """ if self.ns.transport is None: return # XXX: perhaps we should be more vocal about it? if self.ns.transport not in get_all_transports(): logger.error("The selected transport %r is not available", self.ns.transport) return transport_cls = get_all_transports()[self.ns.transport] try: self._transport = transport_cls(self.ns.transport_where, self.ns.transport_options) except ValueError as exc: raise SystemExit(str(exc)) def create_manager(self, storage): """ Create or resume a session that handles most of the stuff needed to run jobs. This sets the attr:`_manager` which enables :meth:`manager`, :meth:`state` and :meth:`storage` properties. The created session state has the on_job_added signal connected to :meth:`on_job_added()`. :raises SessionResumeError: If the session cannot be resumed for any reason. """ all_units = list( itertools.chain(*[p.unit_list for p in self.provider_list])) try: if storage is not None: self._manager = SessionManager.load_session(all_units, storage) else: self._manager = SessionManager.create_with_unit_list(all_units) except DependencyDuplicateError as exc: # Handle possible DependencyDuplicateError that can happen if # someone is using plainbox for job development. print( self.C.RED( _("The job database you are currently using is broken"))) print( self.C.RED( _("At least two jobs contend for the id {0}").format( exc.job.id))) print( self.C.RED( _("First job defined in: {0}").format(exc.job.origin))) print( self.C.RED( _("Second job defined in: {0}").format( exc.duplicate_job.origin))) raise SystemExit(exc) except SessionResumeError as exc: print(self.C.RED(exc)) print(self.C.RED(_("This session cannot be resumed"))) raise else: # Connect the on_job_added signal. We use it to mark the test loop # for re-execution and to update the list of desired jobs. self.state.on_job_added.connect(self.on_job_added) def create_runner(self): """ Create a job runner. This sets the attr:`_runner` which enables :meth:`runner` property. Requires the manager to be created (we need the storage object) """ self._runner = JobRunner( self.storage.location, self.provider_list, # TODO: tie this with well-known-dirs helper os.path.join(self.storage.location, 'io-logs'), command_io_delegate=self, dry_run=self.ns.dry_run) def store_application_metadata(self): """ Store application meta-data (app_id, app_blob) and session title """ self.metadata.title = self.expected_session_title self.metadata.app_id = self.expected_app_id self.metadata.app_blob = b'' @property def expected_app_id(self): return 'plainbox' @property def expected_session_title(self): return " ".join([os.path.basename(sys.argv[0])] + sys.argv[1:]) def find_test_plan(self): # This is using getattr because the code is shared with checkbox-ng # that doesn't support the same set of command line options. test_plan_id = getattr(self.ns, "test_plan", None) if test_plan_id is None: return for provider in self.provider_list: for unit in provider.id_map[test_plan_id]: if unit.Meta.name == 'test plan': return unit def set_effective_categories(self): if self._test_plan is None: return ecm = self._test_plan.get_effective_category_map(self.state.job_list) for job_id, effective_category_id in ecm.items(): job_state = self.state.job_state_map[job_id] job_state.effective_category_id = effective_category_id def do_initial_job_selection(self): """ Compute the initial list of desired jobs """ # Compute the desired job list, this can give us notification about # problems in the selected jobs. Currently we just display each problem desired_job_list = self._get_matching_job_list(self.ns, self.state.job_list) print(self.C.header(_("Analyzing Jobs"))) self._update_desired_job_list(desired_job_list) # Search each provider for the desired test plan if self.ns.test_plan is not None: # TODO: add high-level unit lookup functions for provider in self.provider_list: for unit in provider.id_map.get(self.ns.test_plan, ()): if unit.Meta.name == 'test plan': self.manager.test_plans = (unit, ) break def maybe_warm_up_authentication(self): """ Ask the password before anything else in order to run jobs requiring privileges """ warm_up_list = self.runner.get_warm_up_sequence(self.state.run_list) if warm_up_list: print(self.C.header(_("Authentication"))) for warm_up_func in warm_up_list: warm_up_func() def run_all_selected_jobs(self): """ Run all jobs according to the run list. """ print(self.C.header(_("Running Selected Jobs"))) self._backtrack_and_run_missing = True while self._backtrack_and_run_missing: self._backtrack_and_run_missing = False jobs_to_run = [] estimated_time = 0 # gather jobs that we want to run and skip the jobs that already # have result, this is only needed when we run over the list of # jobs again for job in self.state.run_list: job_state = self.state.job_state_map[job.id] if job_state.result.outcome is None: jobs_to_run.append(job) if (job.estimated_duration is not None and estimated_time is not None): estimated_time += job.estimated_duration else: estimated_time = None for job_no, job in enumerate(jobs_to_run, start=1): print( self.C.header(_( 'Running job {} / {}. Estimated time left: {}').format( job_no, len(jobs_to_run), seconds_to_human_duration(max(0, estimated_time)) if estimated_time is not None else _("unknown")), fill='-')) self.run_single_job(job) if (job.estimated_duration is not None and estimated_time is not None): estimated_time -= job.estimated_duration def run_single_job(self, job): self.run_single_job_with_ui(job, self.get_ui_for_job(job)) def get_ui_for_job(self, job): if self.ns.dont_suppress_output is False and ( job.plugin in ('resource', 'attachment') or 'suppress-output' in job.get_flag_set()): return NormalUI(self.C.c, show_cmd_output=False) else: return NormalUI(self.C.c, show_cmd_output=True) def run_single_job_with_ui(self, job, ui): job_start_time = time.time() job_state = self.state.job_state_map[job.id] ui.considering_job(job, job_state) if job_state.can_start(): ui.about_to_start_running(job, job_state) self.metadata.running_job_name = job.id self.manager.checkpoint() ui.started_running(job, job_state) result_builder = self._run_single_job_with_ui_loop( job, job_state, ui) assert result_builder is not None result_builder.execution_duration = time.time() - job_start_time job_result = result_builder.get_result() self.metadata.running_job_name = None self.manager.checkpoint() ui.finished_running(job, job_state, job_result) else: # Set the outcome of jobs that cannot start to # OUTCOME_NOT_SUPPORTED _except_ if any of the inhibitors point to # a job with an OUTCOME_SKIP outcome, if that is the case mirror # that outcome. This makes 'skip' stronger than 'not-supported' outcome = IJobResult.OUTCOME_NOT_SUPPORTED for inhibitor in job_state.readiness_inhibitor_list: if (inhibitor.cause == InhibitionCause.FAILED_RESOURCE and 'fail-on-resource' in job.get_flag_set()): outcome = IJobResult.OUTCOME_FAIL break elif inhibitor.cause != InhibitionCause.FAILED_DEP: continue related_job_state = self.state.job_state_map[ inhibitor.related_job.id] if related_job_state.result.outcome == IJobResult.OUTCOME_SKIP: outcome = IJobResult.OUTCOME_SKIP result_builder = JobResultBuilder( outcome=outcome, comments=job_state.get_readiness_description(), execution_duration=time.time() - job_start_time) job_result = result_builder.get_result() ui.job_cannot_start(job, job_state, job_result) self.state.update_job_result(job, job_result) ui.finished(job, job_state, job_result) def _run_single_job_with_ui_loop(self, job, job_state, ui): comments = "" while True: if job.plugin in ('user-interact', 'user-interact-verify', 'user-verify', 'manual'): ui.notify_about_purpose(job) if (self.is_interactive and job.plugin in ('user-interact', 'user-interact-verify', 'manual')): ui.notify_about_steps(job) if job.plugin == 'manual': cmd = 'run' else: cmd = ui.wait_for_interaction_prompt(job) if cmd == 'run' or cmd is None: result_builder = self.runner.run_job( job, job_state, self.config, ui).get_builder(comments=comments) elif cmd == 'comment': new_comment = input( self.C.BLUE( _('Please enter your comments:') + '\n')) if new_comment: comments += new_comment + '\n' continue elif cmd == 'skip': result_builder = JobResultBuilder( outcome=IJobResult.OUTCOME_SKIP, comments=_("Explicitly skipped before" " execution")) if comments != "": result_builder.comments = comments break elif cmd == 'quit': raise SystemExit() else: result_builder = self.runner.run_job( job, job_state, self.config, ui).get_builder() else: if 'noreturn' in job.get_flag_set(): ui.noreturn_job() result_builder = self.runner.run_job(job, job_state, self.config, ui).get_builder() if (self.is_interactive and result_builder.outcome == IJobResult.OUTCOME_UNDECIDED): try: if comments != "": result_builder.comments = comments ui.notify_about_verification(job) self._interaction_callback(self.runner, job, result_builder, self.config) except ReRunJob: continue break return result_builder def export_and_send_results(self): # Get a stream with exported session data. exported_stream = io.BytesIO() self.exporter.dump_from_session_manager(self.manager, exported_stream) exported_stream.seek(0) # Need to rewind the file, puagh # Write the stream to file if requested self._save_results(self.ns.output_file, exported_stream) # Invoke the transport? if self.transport is not None: exported_stream.seek(0) try: self._transport.send(exported_stream.read(), self.config, self.state) except TransportError as exc: print(str(exc)) def _save_results(self, output_file, input_stream): if output_file is sys.stdout: print(self.C.header(_("Results"))) # This requires a bit more finesse, as exporters output bytes # and stdout needs a string. translating_stream = ByteStringStreamTranslator( output_file, "utf-8") copyfileobj(input_stream, translating_stream) else: print(_("Saving results to {}").format(output_file.name)) copyfileobj(input_stream, output_file) if output_file is not sys.stdout: output_file.close() def _pick_action_cmd(self, action_list, prompt=None): return ActionUI(action_list, prompt, self._color).run() def _interaction_callback(self, runner, job, result_builder, config, prompt=None, allowed_outcome=None): result = result_builder.get_result() if prompt is None: prompt = _("Select an outcome or an action: ") if allowed_outcome is None: allowed_outcome = [ IJobResult.OUTCOME_PASS, IJobResult.OUTCOME_FAIL, IJobResult.OUTCOME_SKIP ] allowed_actions = [Action('c', _('add a comment'), 'set-comments')] if IJobResult.OUTCOME_PASS in allowed_outcome: allowed_actions.append( Action( 'p', _('set outcome to {0}').format( self.C.GREEN(C_('set outcome to <pass>', 'pass'))), 'set-pass')) if IJobResult.OUTCOME_FAIL in allowed_outcome: allowed_actions.append( Action( 'f', _('set outcome to {0}').format( self.C.RED(C_('set outcome to <fail>', 'fail'))), 'set-fail')) if IJobResult.OUTCOME_SKIP in allowed_outcome: allowed_actions.append( Action( 's', _('set outcome to {0}').format( self.C.YELLOW(C_('set outcome to <skip>', 'skip'))), 'set-skip')) if job.command is not None: allowed_actions.append(Action('r', _('re-run this job'), 're-run')) if result.return_code is not None: if result.return_code == 0: suggested_outcome = IJobResult.OUTCOME_PASS else: suggested_outcome = IJobResult.OUTCOME_FAIL allowed_actions.append( Action( '', _('set suggested outcome [{0}]').format( tr_outcome(suggested_outcome)), 'set-suggested')) while result.outcome not in allowed_outcome: print(_("Please decide what to do next:")) print(" " + _("outcome") + ": {0}".format(self.C.result(result))) if result.comments is None: print(" " + _("comments") + ": {0}".format(C_("none comment", "none"))) else: print( " " + _("comments") + ": {0}".format(self.C.CYAN(result.comments, bright=False))) cmd = self._pick_action_cmd(allowed_actions) # let's store new_comment early for verification if comment has # already been added in the current UI step new_comment = '' if cmd == 'set-pass': result_builder.outcome = IJobResult.OUTCOME_PASS elif cmd == 'set-fail': if 'explicit-fail' in job.get_flag_set() and not new_comment: new_comment = input( self.C.BLUE(_('Please enter your comments:') + '\n')) if new_comment: result_builder.add_comment(new_comment) result_builder.outcome = IJobResult.OUTCOME_FAIL elif cmd == 'set-skip' or cmd is None: result_builder.outcome = IJobResult.OUTCOME_SKIP elif cmd == 'set-suggested': result_builder.outcome = suggested_outcome elif cmd == 'set-comments': new_comment = input( self.C.BLUE(_('Please enter your comments:') + '\n')) if new_comment: result_builder.add_comment(new_comment) elif cmd == 're-run': raise ReRunJob result = result_builder.get_result() def _update_desired_job_list(self, desired_job_list): problem_list = self.state.update_desired_job_list(desired_job_list) if problem_list: print(self.C.header(_("Warning"), 'YELLOW')) print(_("There were some problems with the selected jobs")) for problem in problem_list: print(" * {}".format(problem)) print(_("Problematic jobs will not be considered")) def print_estimated_duration(self): print(self.C.header(_("Session Statistics"))) print( _("This session is about {0:.2f}{percent} complete").format( self.get_completion_ratio() * 100, percent='%')) (estimated_duration_auto, estimated_duration_manual) = self.state.get_estimated_duration() if estimated_duration_auto: print( _("Estimated duration is {:.2f} for automated jobs.").format( estimated_duration_auto)) else: print( _("Estimated duration cannot be determined for automated jobs." )) if estimated_duration_manual: print( _("Estimated duration is {:.2f} for manual jobs.").format( estimated_duration_manual)) else: print( _("Estimated duration cannot be determined for manual jobs.")) print( _("Size of the desired job list: {0}").format( len(self.state.desired_job_list))) print( _("Size of the effective execution plan: {0}").format( len(self.state.run_list))) def get_completion_ratio(self): total_cnt = len(self.state.run_list) total_time = 0 done_cnt = 0 done_time = 0 time_reliable = True for job in self.state.run_list: inc = job.estimated_duration if inc is None: time_reliable = False continue total_time += inc if self.state.job_state_map[job.id].result.outcome is not None: done_cnt += 1 done_time += inc if time_reliable: if total_time == 0: return 0 else: return done_time / total_time else: if total_cnt == 0: return 0 else: return done_cnt / total_cnt def on_job_added(self, job): """ Handler connected to SessionState.on_job_added() The goal of this handler is to re-select all desired jobs (based on original command line arguments and new list of known jobs) and set the backtrack_and_run_missing flag that is observed by _run_all_selected_jobs() """ new_matching_job_list = self._get_matching_job_list( self.ns, self.state.job_list) self._update_desired_job_list(new_matching_job_list) if self._test_plan is not None: job_state = self.state.job_state_map[job.id] job_state.effective_category_id = ( self._test_plan.get_effective_category(job)) self._backtrack_and_run_missing = True
def __init__(self, color, show_cmd_output=True): self.show_cmd_output = show_cmd_output self.C = Colorizer(color) self._color = color
class NormalUI(IJobRunnerUI): STREAM_MAP = {'stdout': sys.stdout, 'stderr': sys.stderr} def __init__(self, color, show_cmd_output=True): self.show_cmd_output = show_cmd_output self.C = Colorizer(color) self._color = color def considering_job(self, job, job_state): print(self.C.header(job.tr_summary(), fill='-')) print(_("ID: {0}").format(job.id)) print(_("Category: {0}").format(job_state.effective_category_id)) def about_to_start_running(self, job, job_state): pass def wait_for_interaction_prompt(self, job): return self.pick_action_cmd([ Action('', _("press ENTER to continue"), 'run'), Action('c', _('add a comment'), 'comment'), Action('s', _("skip this job"), 'skip'), Action('q', _("save the session and quit"), 'quit') ]) def started_running(self, job, job_state): pass def about_to_execute_program(self, args, kwargs): if self.show_cmd_output: print(self.C.BLACK("... 8< -".ljust(80, '-'))) else: print(self.C.BLACK("(" + _("Command output hidden") + ")")) def got_program_output(self, stream_name, line): if not self.show_cmd_output: return stream = self.STREAM_MAP[stream_name] stream = {'stdout': sys.stdout, 'stderr': sys.stderr}[stream_name] if stream_name == 'stdout': print(self.C.GREEN(line.decode("UTF-8", "ignore")), end='', file=stream) elif stream_name == 'stderr': print(self.C.RED(line.decode("UTF-8", "ignore")), end='', file=stream) stream.flush() def finished_executing_program(self, returncode): if self.show_cmd_output: print(self.C.BLACK("- >8 ---".rjust(80, '-'))) def finished_running(self, job, state, result): pass def notify_about_description(self, job): if job.tr_description() is not None: print(self.C.CYAN(job.tr_description())) def notify_about_purpose(self, job): if job.tr_purpose() is not None: print(self.C.WHITE(_("Purpose:"))) print() print(self.C.CYAN(job.tr_purpose())) print() else: self.notify_about_description(job) def notify_about_steps(self, job): if job.tr_steps() is not None: print(self.C.WHITE(_("Steps:"))) print() print(self.C.CYAN(job.tr_steps())) print() def notify_about_verification(self, job): if job.tr_verification() is not None: print(self.C.WHITE(_("Verification:"))) print() print(self.C.CYAN(job.tr_verification())) print() def job_cannot_start(self, job, job_state, result): print(_("Job cannot be started because:")) for inhibitor in job_state.readiness_inhibitor_list: print(" - {}".format(self.C.YELLOW(inhibitor))) def finished(self, job, job_state, result): self._print_result_outcome(result) def _print_result_outcome(self, result): print(_("Outcome") + ": " + self.C.result(result)) def pick_action_cmd(self, action_list, prompt=None): return ActionUI(action_list, prompt, self._color).run() def noreturn_job(self): print( self.C.RED(_("Waiting for the system to shut down or" " reboot...")))
def __init__(self, option_list=None, color=None, exporter_unit=None): super().__init__(option_list, exporter_unit=exporter_unit) self.C = Colorizer(color)
def test_is_enabled(self): """ Ensure that .is_enabled reflects the actual colors """ self.assertTrue(Colorizer(True).is_enabled) self.assertFalse(Colorizer(False).is_enabled)