2017-03-06 11:10:25 +00:00
|
|
|
# Copyright 2013-2015 ARM Limited
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
#
|
|
|
|
|
|
|
|
# pylint: disable=no-member
|
|
|
|
|
2018-05-24 14:51:04 +01:00
|
|
|
import hashlib
|
2017-02-21 13:37:11 +00:00
|
|
|
import logging
|
2018-03-16 17:43:02 +00:00
|
|
|
import os
|
|
|
|
import shutil
|
2017-02-21 13:37:11 +00:00
|
|
|
from copy import copy
|
2017-03-06 11:10:25 +00:00
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
import wa.framework.signal as signal
|
2018-02-16 14:09:02 +00:00
|
|
|
from wa.framework import instrument
|
2017-12-04 17:13:38 +00:00
|
|
|
from wa.framework.configuration.core import Status
|
2018-02-28 10:24:56 +00:00
|
|
|
from wa.framework.exception import TargetError, HostError, WorkloadError,\
|
|
|
|
TargetNotRespondingError, TimeoutError
|
2017-09-18 16:12:03 +01:00
|
|
|
from wa.framework.job import Job
|
2017-03-16 17:54:48 +00:00
|
|
|
from wa.framework.output import init_job_output
|
2018-01-12 15:22:11 +00:00
|
|
|
from wa.framework.output_processor import ProcessorManager
|
2017-02-21 13:37:11 +00:00
|
|
|
from wa.framework.resource import ResourceResolver
|
2017-03-09 14:44:26 +00:00
|
|
|
from wa.framework.target.manager import TargetManager
|
|
|
|
from wa.utils import log
|
2017-12-04 17:22:20 +00:00
|
|
|
from wa.utils.misc import merge_config_values, format_duration
|
2017-02-21 13:37:11 +00:00
|
|
|
|
|
|
|
|
2017-03-06 11:10:25 +00:00
|
|
|
class ExecutionContext(object):
|
|
|
|
|
2017-03-09 14:44:26 +00:00
|
|
|
@property
|
|
|
|
def previous_job(self):
|
2018-05-24 14:17:26 +01:00
|
|
|
if not self.completed_jobs:
|
2017-03-09 14:44:26 +00:00
|
|
|
return None
|
2018-05-24 14:17:26 +01:00
|
|
|
return self.completed_jobs[-1]
|
2017-03-09 14:44:26 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def next_job(self):
|
2018-05-24 14:17:26 +01:00
|
|
|
if not self.job_queue:
|
2017-03-09 14:44:26 +00:00
|
|
|
return None
|
2018-05-24 14:17:26 +01:00
|
|
|
return self.job_queue[0]
|
2017-03-09 14:44:26 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def spec_changed(self):
|
|
|
|
if self.previous_job is None and self.current_job is not None: # Start of run
|
|
|
|
return True
|
|
|
|
if self.previous_job is not None and self.current_job is None: # End of run
|
|
|
|
return True
|
|
|
|
return self.current_job.spec.id != self.previous_job.spec.id
|
|
|
|
|
|
|
|
@property
|
|
|
|
def spec_will_change(self):
|
|
|
|
if self.current_job is None and self.next_job is not None: # Start of run
|
|
|
|
return True
|
|
|
|
if self.current_job is not None and self.next_job is None: # End of run
|
|
|
|
return True
|
|
|
|
return self.current_job.spec.id != self.next_job.spec.id
|
|
|
|
|
2017-09-15 13:41:24 +01:00
|
|
|
@property
|
|
|
|
def workload(self):
|
|
|
|
if self.current_job:
|
|
|
|
return self.current_job.workload
|
|
|
|
|
2017-03-09 16:26:50 +00:00
|
|
|
@property
|
2017-03-16 17:54:48 +00:00
|
|
|
def job_output(self):
|
|
|
|
if self.current_job:
|
|
|
|
return self.current_job.output
|
|
|
|
|
|
|
|
@property
|
|
|
|
def output(self):
|
2017-03-09 16:26:50 +00:00
|
|
|
if self.current_job:
|
2017-03-16 17:54:48 +00:00
|
|
|
return self.job_output
|
|
|
|
return self.run_output
|
|
|
|
|
|
|
|
@property
|
|
|
|
def output_directory(self):
|
2017-03-09 16:26:50 +00:00
|
|
|
return self.output.basepath
|
2017-03-06 11:10:25 +00:00
|
|
|
|
2018-05-22 17:45:59 +01:00
|
|
|
@property
|
|
|
|
def reboot_policy(self):
|
|
|
|
return self.cm.run_config.reboot_policy
|
|
|
|
|
2018-06-22 16:50:49 +01:00
|
|
|
@property
|
|
|
|
def target_info(self):
|
|
|
|
return self.run_output.target_info
|
|
|
|
|
2017-03-06 11:10:25 +00:00
|
|
|
def __init__(self, cm, tm, output):
|
2017-03-09 14:44:26 +00:00
|
|
|
self.logger = logging.getLogger('context')
|
2017-03-06 11:10:25 +00:00
|
|
|
self.cm = cm
|
|
|
|
self.tm = tm
|
2017-03-16 17:54:48 +00:00
|
|
|
self.run_output = output
|
|
|
|
self.run_state = output.state
|
2017-03-06 11:10:25 +00:00
|
|
|
self.logger.debug('Loading resource discoverers')
|
2017-03-21 16:00:18 +00:00
|
|
|
self.resolver = ResourceResolver(cm.plugin_cache)
|
2017-03-06 11:10:25 +00:00
|
|
|
self.resolver.load()
|
2017-03-09 14:44:26 +00:00
|
|
|
self.job_queue = None
|
|
|
|
self.completed_jobs = None
|
|
|
|
self.current_job = None
|
2017-03-20 16:24:22 +00:00
|
|
|
self.successful_jobs = 0
|
|
|
|
self.failed_jobs = 0
|
2018-03-07 14:39:47 +00:00
|
|
|
self.run_interrupted = False
|
2017-03-09 14:44:26 +00:00
|
|
|
|
|
|
|
def start_run(self):
|
2017-03-20 16:27:58 +00:00
|
|
|
self.output.info.start_time = datetime.utcnow()
|
2017-03-09 14:44:26 +00:00
|
|
|
self.output.write_info()
|
|
|
|
self.job_queue = copy(self.cm.jobs)
|
|
|
|
self.completed_jobs = []
|
2017-03-20 16:24:22 +00:00
|
|
|
self.run_state.status = Status.STARTED
|
|
|
|
self.output.status = Status.STARTED
|
2017-03-16 17:54:48 +00:00
|
|
|
self.output.write_state()
|
2017-03-09 14:44:26 +00:00
|
|
|
|
|
|
|
def end_run(self):
|
2017-03-20 16:24:22 +00:00
|
|
|
if self.successful_jobs:
|
|
|
|
if self.failed_jobs:
|
2017-03-22 15:17:14 +00:00
|
|
|
status = Status.PARTIAL
|
2017-03-20 16:24:22 +00:00
|
|
|
else:
|
|
|
|
status = Status.OK
|
|
|
|
else:
|
|
|
|
status = Status.FAILED
|
|
|
|
self.run_state.status = status
|
2018-02-22 09:11:27 +00:00
|
|
|
self.run_output.status = status
|
|
|
|
self.run_output.info.end_time = datetime.utcnow()
|
2018-02-23 12:37:07 +00:00
|
|
|
self.run_output.info.duration = self.run_output.info.end_time -\
|
|
|
|
self.run_output.info.start_time
|
2018-06-22 16:54:30 +01:00
|
|
|
self.write_output()
|
2017-03-09 14:44:26 +00:00
|
|
|
|
2017-10-05 18:25:29 +01:00
|
|
|
def finalize(self):
|
|
|
|
self.tm.finalize()
|
|
|
|
|
2017-03-09 14:44:26 +00:00
|
|
|
def start_job(self):
|
|
|
|
if not self.job_queue:
|
|
|
|
raise RuntimeError('No jobs to run')
|
|
|
|
self.current_job = self.job_queue.pop(0)
|
2018-02-15 10:14:57 +00:00
|
|
|
job_output = init_job_output(self.run_output, self.current_job)
|
|
|
|
self.current_job.set_output(job_output)
|
2017-03-16 17:54:48 +00:00
|
|
|
self.update_job_state(self.current_job)
|
2017-03-09 14:44:26 +00:00
|
|
|
return self.current_job
|
|
|
|
|
|
|
|
def end_job(self):
|
|
|
|
if not self.current_job:
|
|
|
|
raise RuntimeError('No jobs in progress')
|
|
|
|
self.completed_jobs.append(self.current_job)
|
2017-03-16 17:54:48 +00:00
|
|
|
self.update_job_state(self.current_job)
|
|
|
|
self.output.write_result()
|
2017-03-09 14:44:26 +00:00
|
|
|
self.current_job = None
|
2017-03-06 11:10:25 +00:00
|
|
|
|
2017-09-18 16:12:03 +01:00
|
|
|
def set_status(self, status, force=False):
|
2017-09-15 13:41:24 +01:00
|
|
|
if not self.current_job:
|
|
|
|
raise RuntimeError('No jobs in progress')
|
2017-09-18 16:12:03 +01:00
|
|
|
self.current_job.set_status(status, force)
|
2017-09-15 13:41:24 +01:00
|
|
|
|
2017-03-27 17:31:44 +01:00
|
|
|
def extract_results(self):
|
|
|
|
self.tm.extract_results(self)
|
|
|
|
|
2017-03-09 17:39:44 +00:00
|
|
|
def move_failed(self, job):
|
2017-03-16 17:54:48 +00:00
|
|
|
self.run_output.move_failed(job.output)
|
|
|
|
|
|
|
|
def update_job_state(self, job):
|
|
|
|
self.run_state.update_job(job)
|
|
|
|
self.run_output.write_state()
|
2017-03-09 17:39:44 +00:00
|
|
|
|
2017-10-23 16:38:59 +01:00
|
|
|
def skip_job(self, job):
|
|
|
|
job.status = Status.SKIPPED
|
|
|
|
self.run_state.update_job(job)
|
|
|
|
self.completed_jobs.append(job)
|
|
|
|
|
2017-03-17 17:29:30 +00:00
|
|
|
def skip_remaining_jobs(self):
|
|
|
|
while self.job_queue:
|
|
|
|
job = self.job_queue.pop(0)
|
2017-10-23 16:38:59 +01:00
|
|
|
self.skip_job(job)
|
2017-03-17 17:29:30 +00:00
|
|
|
self.write_state()
|
|
|
|
|
2017-03-16 17:54:48 +00:00
|
|
|
def write_state(self):
|
|
|
|
self.run_output.write_state()
|
|
|
|
|
2018-06-22 16:54:30 +01:00
|
|
|
def write_output(self):
|
|
|
|
self.run_output.write_info()
|
|
|
|
self.run_output.write_state()
|
|
|
|
self.run_output.write_result()
|
|
|
|
|
2018-06-22 16:57:14 +01:00
|
|
|
def write_job_specs(self):
|
|
|
|
self.run_output.write_job_specs(self.cm.job_specs)
|
|
|
|
|
2018-05-24 14:51:04 +01:00
|
|
|
def get_resource(self, resource, strict=True):
|
|
|
|
result = self.resolver.get(resource, strict)
|
2018-06-01 13:53:34 +01:00
|
|
|
if result is None:
|
|
|
|
return result
|
2018-05-24 14:51:04 +01:00
|
|
|
if os.path.isfile(result):
|
|
|
|
with open(result, 'rb') as fh:
|
|
|
|
md5hash = hashlib.md5(fh.read())
|
|
|
|
key = '{}/{}'.format(resource.owner, os.path.basename(result))
|
|
|
|
self.update_metadata('hashes', key, md5hash.hexdigest())
|
|
|
|
return result
|
|
|
|
|
|
|
|
get = get_resource # alias to allow a context to act as a resolver
|
|
|
|
|
2017-09-15 13:41:24 +01:00
|
|
|
def get_metric(self, name):
|
|
|
|
try:
|
|
|
|
return self.output.get_metric(name)
|
|
|
|
except HostError:
|
|
|
|
if not self.current_job:
|
|
|
|
raise
|
|
|
|
return self.run_output.get_metric(name)
|
|
|
|
|
2017-03-16 17:54:48 +00:00
|
|
|
def add_metric(self, name, value, units=None, lower_is_better=False,
|
|
|
|
classifiers=None):
|
|
|
|
if self.current_job:
|
|
|
|
classifiers = merge_config_values(self.current_job.classifiers,
|
|
|
|
classifiers)
|
|
|
|
self.output.add_metric(name, value, units, lower_is_better, classifiers)
|
|
|
|
|
2017-03-28 09:58:48 +01:00
|
|
|
def get_artifact(self, name):
|
|
|
|
try:
|
|
|
|
return self.output.get_artifact(name)
|
|
|
|
except HostError:
|
|
|
|
if not self.current_job:
|
|
|
|
raise
|
|
|
|
return self.run_output.get_artifact(name)
|
|
|
|
|
|
|
|
def get_artifact_path(self, name):
|
|
|
|
try:
|
|
|
|
return self.output.get_artifact_path(name)
|
|
|
|
except HostError:
|
|
|
|
if not self.current_job:
|
|
|
|
raise
|
|
|
|
return self.run_output.get_artifact_path(name)
|
|
|
|
|
2017-03-16 17:54:48 +00:00
|
|
|
def add_artifact(self, name, path, kind, description=None, classifiers=None):
|
|
|
|
self.output.add_artifact(name, path, kind, description, classifiers)
|
|
|
|
|
|
|
|
def add_run_artifact(self, name, path, kind, description=None,
|
|
|
|
classifiers=None):
|
|
|
|
self.run_output.add_artifact(name, path, kind, description, classifiers)
|
2017-03-06 11:10:25 +00:00
|
|
|
|
2017-03-20 16:24:22 +00:00
|
|
|
def add_event(self, message):
|
|
|
|
self.output.add_event(message)
|
|
|
|
|
2018-05-24 13:59:23 +01:00
|
|
|
def add_metadata(self, key, *args, **kwargs):
|
|
|
|
self.output.add_metadata(key, *args, **kwargs)
|
|
|
|
|
|
|
|
def update_metadata(self, key, *args):
|
|
|
|
self.output.update_metadata(key, *args)
|
|
|
|
|
2018-03-16 17:43:02 +00:00
|
|
|
def take_screenshot(self, filename):
|
|
|
|
filepath = self._get_unique_filepath(filename)
|
|
|
|
self.tm.target.capture_screen(filepath)
|
2018-05-23 14:56:35 +01:00
|
|
|
if os.path.isfile(filepath):
|
|
|
|
self.add_artifact('screenshot', filepath, kind='log')
|
2018-03-16 17:43:02 +00:00
|
|
|
|
|
|
|
def take_uiautomator_dump(self, filename):
|
|
|
|
filepath = self._get_unique_filepath(filename)
|
|
|
|
self.tm.target.capture_ui_hierarchy(filepath)
|
|
|
|
self.add_artifact('uitree', filepath, kind='log')
|
|
|
|
|
|
|
|
def record_ui_state(self, basename):
|
|
|
|
self.logger.info('Recording screen state...')
|
|
|
|
self.take_screenshot('{}.png'.format(basename))
|
|
|
|
target = self.tm.target
|
|
|
|
if target.os == 'android' or\
|
|
|
|
(target.os == 'chromeos' and target.has('android_container')):
|
|
|
|
self.take_uiautomator_dump('{}.uix'.format(basename))
|
|
|
|
|
2017-10-23 16:41:28 +01:00
|
|
|
def initialize_jobs(self):
|
|
|
|
new_queue = []
|
|
|
|
failed_ids = []
|
|
|
|
for job in self.job_queue:
|
|
|
|
if job.id in failed_ids:
|
|
|
|
# Don't try to initialize a job if another job with the same ID
|
|
|
|
# (i.e. same job spec) has failed - we can assume it will fail
|
|
|
|
# too.
|
|
|
|
self.skip_job(job)
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
job.initialize(self)
|
|
|
|
except WorkloadError as e:
|
|
|
|
job.set_status(Status.FAILED)
|
2018-02-28 15:18:58 +00:00
|
|
|
log.log_error(e, self.logger)
|
2017-10-23 16:41:28 +01:00
|
|
|
failed_ids.append(job.id)
|
|
|
|
|
|
|
|
if self.cm.run_config.bail_on_init_failure:
|
|
|
|
raise
|
|
|
|
else:
|
|
|
|
new_queue.append(job)
|
|
|
|
|
|
|
|
self.job_queue = new_queue
|
|
|
|
|
2018-03-16 17:43:02 +00:00
|
|
|
def _get_unique_filepath(self, filename):
|
|
|
|
filepath = os.path.join(self.output_directory, filename)
|
|
|
|
rest, ext = os.path.splitext(filepath)
|
|
|
|
i = 1
|
|
|
|
new_filepath = '{}-{}{}'.format(rest, i, ext)
|
|
|
|
|
|
|
|
if not os.path.exists(filepath) and not os.path.exists(new_filepath):
|
|
|
|
return filepath
|
|
|
|
elif not os.path.exists(new_filepath):
|
|
|
|
# new_filepath does not exit, thefore filepath must exit.
|
|
|
|
# this is the first collision
|
|
|
|
shutil.move(filepath, new_filepath)
|
|
|
|
|
|
|
|
while os.path.exists(new_filepath):
|
|
|
|
i += 1
|
|
|
|
new_filepath = '{}-{}{}'.format(rest, i, ext)
|
|
|
|
return new_filepath
|
|
|
|
|
2017-03-06 11:10:25 +00:00
|
|
|
|
|
|
|
class Executor(object):
|
|
|
|
"""
|
|
|
|
The ``Executor``'s job is to set up the execution context and pass to a
|
|
|
|
``Runner`` along with a loaded run specification. Once the ``Runner`` has
|
2017-04-12 14:47:34 +01:00
|
|
|
done its thing, the ``Executor`` performs some final reporting before
|
2017-03-06 11:10:25 +00:00
|
|
|
returning.
|
|
|
|
|
|
|
|
The initial context set up involves combining configuration from various
|
|
|
|
sources, loading of requided workloads, loading and installation of
|
2018-01-12 15:22:11 +00:00
|
|
|
instruments and output processors, etc. Static validation of the combined
|
2017-03-06 11:10:25 +00:00
|
|
|
configuration is also performed.
|
2017-02-21 13:37:11 +00:00
|
|
|
|
|
|
|
"""
|
2017-03-06 11:10:25 +00:00
|
|
|
# pylint: disable=R0915
|
|
|
|
|
|
|
|
def __init__(self):
|
2017-03-09 14:44:26 +00:00
|
|
|
self.logger = logging.getLogger('executor')
|
2017-03-06 11:10:25 +00:00
|
|
|
self.error_logged = False
|
|
|
|
self.warning_logged = False
|
2017-04-12 14:04:28 +01:00
|
|
|
self.target_manager = None
|
2017-03-06 11:10:25 +00:00
|
|
|
|
|
|
|
def execute(self, config_manager, output):
|
|
|
|
"""
|
|
|
|
Execute the run specified by an agenda. Optionally, selectors may be
|
|
|
|
used to only selecute a subset of the specified agenda.
|
|
|
|
|
|
|
|
Params::
|
|
|
|
|
2018-01-29 09:47:52 +00:00
|
|
|
:state: a ``ConfigManager`` containing processed configuration
|
2017-03-06 11:10:25 +00:00
|
|
|
:output: an initialized ``RunOutput`` that will be used to
|
|
|
|
store the results.
|
|
|
|
|
|
|
|
"""
|
|
|
|
signal.connect(self._error_signalled_callback, signal.ERROR_LOGGED)
|
|
|
|
signal.connect(self._warning_signalled_callback, signal.WARNING_LOGGED)
|
|
|
|
|
2018-06-22 16:58:54 +01:00
|
|
|
def execute(self, config_manager, output):
|
2017-03-06 11:10:25 +00:00
|
|
|
self.logger.info('Initializing run')
|
|
|
|
self.logger.debug('Finalizing run configuration.')
|
|
|
|
config = config_manager.finalize()
|
|
|
|
output.write_config(config)
|
|
|
|
|
2017-04-12 14:04:28 +01:00
|
|
|
self.target_manager = TargetManager(config.run_config.device,
|
2017-10-05 15:17:58 +01:00
|
|
|
config.run_config.device_config,
|
|
|
|
output.basepath)
|
2018-05-22 14:55:21 +01:00
|
|
|
|
2018-06-22 16:58:54 +01:00
|
|
|
self.logger.info('Initializing execution context')
|
|
|
|
context = ExecutionContext(config_manager, self.target_manager, output)
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.do_execute(context)
|
|
|
|
except KeyboardInterrupt as e:
|
|
|
|
context.run_output.status = 'ABORTED'
|
|
|
|
log.log_error(e, self.logger)
|
|
|
|
context.write_output()
|
|
|
|
raise
|
|
|
|
except Exception as e:
|
|
|
|
context.run_output.status = 'FAILED'
|
|
|
|
log.log_error(e, self.logger)
|
|
|
|
context.write_output()
|
|
|
|
raise
|
|
|
|
finally:
|
|
|
|
context.finalize()
|
|
|
|
self.execute_postamble(context, output)
|
|
|
|
signal.send(signal.RUN_COMPLETED, self, context)
|
|
|
|
|
|
|
|
def do_execute(self, context):
|
|
|
|
self.logger.info('Connecting to target')
|
|
|
|
context.tm.initialize()
|
|
|
|
|
|
|
|
if context.cm.run_config.reboot_policy.perform_initial_reboot:
|
|
|
|
self.logger.info('Performing initial reboot.')
|
|
|
|
attempts = context.cm.run_config.max_retries
|
2018-05-24 14:03:03 +01:00
|
|
|
while attempts:
|
|
|
|
try:
|
2018-05-29 09:40:39 +01:00
|
|
|
self.target_manager.reboot()
|
2018-05-24 14:03:03 +01:00
|
|
|
except TargetError as e:
|
|
|
|
if attempts:
|
|
|
|
attempts -= 1
|
|
|
|
else:
|
|
|
|
raise e
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
2018-06-22 16:58:54 +01:00
|
|
|
context.output.set_target_info(self.target_manager.get_target_info())
|
2017-03-06 11:10:25 +00:00
|
|
|
|
|
|
|
self.logger.info('Generating jobs')
|
2018-06-22 16:58:54 +01:00
|
|
|
context.cm.generate_jobs(context)
|
|
|
|
context.write_job_specs()
|
|
|
|
context.output.write_state()
|
2017-03-06 11:10:25 +00:00
|
|
|
|
2018-01-10 14:54:43 +00:00
|
|
|
self.logger.info('Installing instruments')
|
2018-06-22 16:58:54 +01:00
|
|
|
for instrument_name in context.cm.get_instruments(self.target_manager.target):
|
2018-02-16 14:09:02 +00:00
|
|
|
instrument.install(instrument_name, context)
|
|
|
|
instrument.validate()
|
2017-03-06 11:10:25 +00:00
|
|
|
|
2018-01-12 15:22:11 +00:00
|
|
|
self.logger.info('Installing output processors')
|
2017-03-20 16:24:22 +00:00
|
|
|
pm = ProcessorManager()
|
2018-06-22 16:58:54 +01:00
|
|
|
for proc in context.cm.get_processors():
|
2017-10-23 12:25:10 +01:00
|
|
|
pm.install(proc, context)
|
2017-03-20 16:24:22 +00:00
|
|
|
pm.validate()
|
|
|
|
|
2017-03-09 14:44:26 +00:00
|
|
|
self.logger.info('Starting run')
|
2017-03-20 16:24:22 +00:00
|
|
|
runner = Runner(context, pm)
|
2018-06-12 14:47:26 +01:00
|
|
|
signal.send(signal.RUN_STARTED, self, context)
|
2018-06-22 16:58:54 +01:00
|
|
|
runner.run()
|
2017-03-09 14:44:26 +00:00
|
|
|
|
2017-03-17 17:05:40 +00:00
|
|
|
def execute_postamble(self, context, output):
|
2017-03-06 11:10:25 +00:00
|
|
|
self.logger.info('Done.')
|
2017-03-17 17:05:40 +00:00
|
|
|
duration = format_duration(output.info.duration)
|
|
|
|
self.logger.info('Run duration: {}'.format(duration))
|
|
|
|
num_ran = context.run_state.num_completed_jobs
|
|
|
|
status_summary = 'Ran a total of {} iterations: '.format(num_ran)
|
|
|
|
|
|
|
|
counter = context.run_state.get_status_counts()
|
2017-03-06 11:10:25 +00:00
|
|
|
parts = []
|
2017-10-11 17:24:09 +01:00
|
|
|
for status in reversed(Status.levels):
|
2017-03-06 11:10:25 +00:00
|
|
|
if status in counter:
|
|
|
|
parts.append('{} {}'.format(counter[status], status))
|
|
|
|
self.logger.info(status_summary + ', '.join(parts))
|
2017-03-17 17:05:40 +00:00
|
|
|
|
|
|
|
self.logger.info('Results can be found in {}'.format(output.basepath))
|
2017-03-06 11:10:25 +00:00
|
|
|
|
|
|
|
if self.error_logged:
|
|
|
|
self.logger.warn('There were errors during execution.')
|
2017-03-17 17:05:40 +00:00
|
|
|
self.logger.warn('Please see {}'.format(output.logfile))
|
2017-03-06 11:10:25 +00:00
|
|
|
elif self.warning_logged:
|
|
|
|
self.logger.warn('There were warnings during execution.')
|
2017-03-17 17:05:40 +00:00
|
|
|
self.logger.warn('Please see {}'.format(output.logfile))
|
2017-03-06 11:10:25 +00:00
|
|
|
|
2018-03-07 11:32:19 +00:00
|
|
|
def _error_signalled_callback(self, record):
|
2017-03-06 11:10:25 +00:00
|
|
|
self.error_logged = True
|
|
|
|
signal.disconnect(self._error_signalled_callback, signal.ERROR_LOGGED)
|
|
|
|
|
2018-03-07 11:32:19 +00:00
|
|
|
def _warning_signalled_callback(self, record):
|
2017-03-06 11:10:25 +00:00
|
|
|
self.warning_logged = True
|
|
|
|
signal.disconnect(self._warning_signalled_callback, signal.WARNING_LOGGED)
|
|
|
|
|
2018-05-09 16:19:18 +01:00
|
|
|
def __str__(self):
|
|
|
|
return 'executor'
|
|
|
|
|
|
|
|
__repr__ = __str__
|
|
|
|
|
2017-02-21 13:37:11 +00:00
|
|
|
|
2017-03-06 11:10:25 +00:00
|
|
|
class Runner(object):
|
2017-02-21 13:37:11 +00:00
|
|
|
"""
|
2017-12-06 14:46:23 +00:00
|
|
|
Triggers running jobs and processing results
|
2017-10-04 13:24:44 +01:00
|
|
|
|
2017-12-06 14:46:23 +00:00
|
|
|
Takes pre-initialized ExcecutionContext and ProcessorManager. Handles
|
|
|
|
actually running the jobs, and triggers the ProcessorManager to handle
|
|
|
|
processing job and run results.
|
2017-02-21 13:37:11 +00:00
|
|
|
"""
|
|
|
|
|
2017-03-20 16:24:22 +00:00
|
|
|
def __init__(self, context, pm):
|
2017-03-09 14:44:26 +00:00
|
|
|
self.logger = logging.getLogger('runner')
|
|
|
|
self.context = context
|
2017-03-20 16:24:22 +00:00
|
|
|
self.pm = pm
|
2017-03-09 14:44:26 +00:00
|
|
|
self.output = self.context.output
|
|
|
|
self.config = self.context.cm
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
try:
|
|
|
|
self.initialize_run()
|
|
|
|
self.send(signal.RUN_INITIALIZED)
|
|
|
|
|
2018-06-12 14:16:22 +01:00
|
|
|
with signal.wrap('JOB_QUEUE_EXECUTION', self, self.context):
|
|
|
|
while self.context.job_queue:
|
|
|
|
if self.context.run_interrupted:
|
|
|
|
raise KeyboardInterrupt()
|
|
|
|
self.run_next_job(self.context)
|
2018-02-28 10:24:56 +00:00
|
|
|
|
|
|
|
except KeyboardInterrupt as e:
|
|
|
|
log.log_error(e, self.logger)
|
|
|
|
self.logger.info('Skipping remaining jobs.')
|
|
|
|
self.context.skip_remaining_jobs()
|
2017-03-09 14:44:26 +00:00
|
|
|
except Exception as e:
|
2018-05-30 13:58:49 +01:00
|
|
|
message = e.args[0] if e.args else str(e)
|
2018-02-28 10:24:56 +00:00
|
|
|
log.log_error(e, self.logger)
|
|
|
|
self.logger.error('Skipping remaining jobs due to "{}".'.format(e))
|
|
|
|
self.context.skip_remaining_jobs()
|
2017-03-09 14:44:26 +00:00
|
|
|
raise e
|
|
|
|
finally:
|
|
|
|
self.finalize_run()
|
2017-03-17 15:57:05 +00:00
|
|
|
self.send(signal.RUN_FINALIZED)
|
2017-03-09 14:44:26 +00:00
|
|
|
|
|
|
|
def initialize_run(self):
|
|
|
|
self.logger.info('Initializing run')
|
2018-03-07 11:32:19 +00:00
|
|
|
signal.connect(self._error_signalled_callback, signal.ERROR_LOGGED)
|
|
|
|
signal.connect(self._warning_signalled_callback, signal.WARNING_LOGGED)
|
2017-03-09 14:44:26 +00:00
|
|
|
self.context.start_run()
|
2017-03-20 16:24:22 +00:00
|
|
|
self.pm.initialize()
|
2018-05-10 11:44:03 +01:00
|
|
|
with log.indentcontext():
|
|
|
|
self.context.initialize_jobs()
|
2017-03-16 17:54:48 +00:00
|
|
|
self.context.write_state()
|
2017-03-09 14:44:26 +00:00
|
|
|
|
|
|
|
def finalize_run(self):
|
2018-05-11 14:46:01 +01:00
|
|
|
self.logger.info('Run completed')
|
|
|
|
with log.indentcontext():
|
|
|
|
for job in self.context.completed_jobs:
|
|
|
|
job.finalize(self.context)
|
2017-03-09 14:44:26 +00:00
|
|
|
self.logger.info('Finalizing run')
|
|
|
|
self.context.end_run()
|
2018-01-29 09:47:52 +00:00
|
|
|
self.pm.enable_all()
|
2018-06-11 17:50:46 +01:00
|
|
|
with signal.wrap('RUN_OUTPUT_PROCESSED', self):
|
2018-06-04 12:00:35 +01:00
|
|
|
self.pm.process_run_output(self.context)
|
|
|
|
self.pm.export_run_output(self.context)
|
2017-03-20 16:24:22 +00:00
|
|
|
self.pm.finalize()
|
2018-03-07 11:32:19 +00:00
|
|
|
signal.disconnect(self._error_signalled_callback, signal.ERROR_LOGGED)
|
|
|
|
signal.disconnect(self._warning_signalled_callback, signal.WARNING_LOGGED)
|
2017-03-09 14:44:26 +00:00
|
|
|
|
|
|
|
def run_next_job(self, context):
|
|
|
|
job = context.start_job()
|
|
|
|
self.logger.info('Running job {}'.format(job.id))
|
2017-03-09 17:39:44 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
log.indent()
|
2018-05-24 13:55:44 +01:00
|
|
|
if self.context.reboot_policy.reboot_on_each_job:
|
|
|
|
self.logger.info('Rebooting on new job.')
|
2018-06-11 17:14:51 +01:00
|
|
|
self.context.tm.reboot(context)
|
2018-05-24 14:05:47 +01:00
|
|
|
elif self.context.reboot_policy.reboot_on_each_spec and context.spec_changed:
|
|
|
|
self.logger.info('Rebooting on new spec.')
|
2018-06-11 17:14:51 +01:00
|
|
|
self.context.tm.reboot(context)
|
2018-05-24 13:55:44 +01:00
|
|
|
|
2018-05-29 09:20:52 +01:00
|
|
|
with signal.wrap('JOB', self, context):
|
|
|
|
context.tm.start()
|
|
|
|
self.do_run_job(job, context)
|
|
|
|
job.set_status(Status.OK)
|
2018-03-01 17:09:43 +00:00
|
|
|
except (Exception, KeyboardInterrupt) as e: # pylint: disable=broad-except
|
2018-02-28 15:18:58 +00:00
|
|
|
log.log_error(e, self.logger)
|
2018-03-01 17:09:43 +00:00
|
|
|
if isinstance(e, KeyboardInterrupt):
|
2018-03-07 14:39:47 +00:00
|
|
|
context.run_interrupted = True
|
2018-03-01 17:09:43 +00:00
|
|
|
job.set_status(Status.ABORTED)
|
|
|
|
raise e
|
|
|
|
else:
|
|
|
|
job.set_status(Status.FAILED)
|
|
|
|
if isinstance(e, TargetNotRespondingError):
|
2018-02-28 10:24:56 +00:00
|
|
|
raise e
|
|
|
|
elif isinstance(e, TargetError):
|
2018-06-11 17:14:51 +01:00
|
|
|
context.tm.verify_target_responsive(context)
|
2017-03-09 17:39:44 +00:00
|
|
|
finally:
|
|
|
|
self.logger.info('Completing job {}'.format(job.id))
|
|
|
|
self.send(signal.JOB_COMPLETED)
|
2018-05-24 13:45:35 +01:00
|
|
|
context.tm.stop()
|
2017-03-09 17:39:44 +00:00
|
|
|
context.end_job()
|
|
|
|
|
|
|
|
log.dedent()
|
|
|
|
self.check_job(job)
|
|
|
|
|
|
|
|
def do_run_job(self, job, context):
|
2017-10-04 13:25:06 +01:00
|
|
|
rc = self.context.cm.run_config
|
|
|
|
if job.workload.phones_home and not rc.allow_phone_home:
|
|
|
|
self.logger.warning('Skipping job {} ({}) due to allow_phone_home=False'
|
|
|
|
.format(job.id, job.workload.name))
|
2017-10-23 16:38:59 +01:00
|
|
|
self.context.skip_job(job)
|
2017-10-04 13:25:06 +01:00
|
|
|
return
|
|
|
|
|
2017-09-18 16:12:03 +01:00
|
|
|
job.set_status(Status.RUNNING)
|
2017-03-09 14:44:26 +00:00
|
|
|
self.send(signal.JOB_STARTED)
|
|
|
|
|
2018-01-29 09:47:52 +00:00
|
|
|
job.configure_augmentations(context, self.pm)
|
|
|
|
|
2018-01-25 08:40:44 +00:00
|
|
|
with signal.wrap('JOB_TARGET_CONFIG', self, context):
|
2017-03-09 14:44:26 +00:00
|
|
|
job.configure_target(context)
|
|
|
|
|
2018-03-16 17:44:28 +00:00
|
|
|
try:
|
2018-05-30 11:53:22 +01:00
|
|
|
job.setup(context)
|
2018-03-16 17:44:28 +00:00
|
|
|
except Exception as e:
|
|
|
|
job.set_status(Status.FAILED)
|
|
|
|
log.log_error(e, self.logger)
|
|
|
|
if isinstance(e, TargetError) or isinstance(e, TimeoutError):
|
2018-06-11 17:14:51 +01:00
|
|
|
context.tm.verify_target_responsive(context)
|
2018-03-16 17:44:28 +00:00
|
|
|
self.context.record_ui_state('setup-error')
|
|
|
|
raise e
|
2017-10-04 13:24:44 +01:00
|
|
|
|
2017-03-09 14:44:26 +00:00
|
|
|
try:
|
|
|
|
|
|
|
|
try:
|
2018-05-30 11:53:22 +01:00
|
|
|
job.run(context)
|
2018-03-01 17:09:43 +00:00
|
|
|
except KeyboardInterrupt:
|
2018-03-07 14:39:47 +00:00
|
|
|
context.run_interrupted = True
|
2018-03-01 17:09:43 +00:00
|
|
|
job.set_status(Status.ABORTED)
|
|
|
|
raise
|
2017-11-24 08:23:00 +00:00
|
|
|
except Exception as e:
|
|
|
|
job.set_status(Status.FAILED)
|
2018-02-28 15:18:58 +00:00
|
|
|
log.log_error(e, self.logger)
|
2018-02-28 10:24:56 +00:00
|
|
|
if isinstance(e, TargetError) or isinstance(e, TimeoutError):
|
2018-06-11 17:14:51 +01:00
|
|
|
context.tm.verify_target_responsive(context)
|
2018-03-16 17:44:28 +00:00
|
|
|
self.context.record_ui_state('run-error')
|
2017-11-24 08:23:00 +00:00
|
|
|
raise e
|
|
|
|
finally:
|
|
|
|
try:
|
2018-01-25 08:40:44 +00:00
|
|
|
with signal.wrap('JOB_OUTPUT_PROCESSED', self, context):
|
2017-11-24 08:23:00 +00:00
|
|
|
job.process_output(context)
|
2018-05-29 09:50:41 +01:00
|
|
|
self.pm.process_job_output(context)
|
2017-11-24 08:23:00 +00:00
|
|
|
self.pm.export_job_output(context)
|
2018-02-28 10:24:56 +00:00
|
|
|
except Exception as e:
|
2017-11-24 08:23:00 +00:00
|
|
|
job.set_status(Status.PARTIAL)
|
2018-02-28 10:24:56 +00:00
|
|
|
if isinstance(e, TargetError) or isinstance(e, TimeoutError):
|
2018-06-11 17:14:51 +01:00
|
|
|
context.tm.verify_target_responsive(context)
|
2018-03-16 17:44:28 +00:00
|
|
|
self.context.record_ui_state('output-error')
|
2017-11-24 08:23:00 +00:00
|
|
|
raise
|
2017-03-20 16:24:22 +00:00
|
|
|
|
2017-03-09 14:44:26 +00:00
|
|
|
except KeyboardInterrupt:
|
2018-03-07 14:39:47 +00:00
|
|
|
context.run_interrupted = True
|
2017-09-18 16:12:03 +01:00
|
|
|
job.set_status(Status.ABORTED)
|
2017-03-09 14:44:26 +00:00
|
|
|
raise
|
|
|
|
finally:
|
|
|
|
# If setup was successfully completed, teardown must
|
|
|
|
# run even if the job failed
|
2018-05-30 11:53:22 +01:00
|
|
|
job.teardown(context)
|
2017-03-09 14:44:26 +00:00
|
|
|
|
2017-03-09 17:39:44 +00:00
|
|
|
def check_job(self, job):
|
|
|
|
rc = self.context.cm.run_config
|
|
|
|
if job.status in rc.retry_on_status:
|
|
|
|
if job.retries < rc.max_retries:
|
2017-03-16 17:54:48 +00:00
|
|
|
msg = 'Job {} iteration {} completed with status {}. retrying...'
|
2017-12-01 17:03:36 +00:00
|
|
|
self.logger.error(msg.format(job.id, job.iteration, job.status))
|
2017-09-18 16:12:03 +01:00
|
|
|
self.retry_job(job)
|
2017-03-09 17:39:44 +00:00
|
|
|
self.context.move_failed(job)
|
2017-03-16 17:54:48 +00:00
|
|
|
self.context.write_state()
|
2017-03-09 17:39:44 +00:00
|
|
|
else:
|
|
|
|
msg = 'Job {} iteration {} completed with status {}. '\
|
|
|
|
'Max retries exceeded.'
|
2017-09-18 16:12:03 +01:00
|
|
|
self.logger.error(msg.format(job.id, job.iteration, job.status))
|
2017-03-20 16:24:22 +00:00
|
|
|
self.context.failed_jobs += 1
|
2018-05-29 11:36:05 +01:00
|
|
|
self.send(signal.JOB_FAILED)
|
2017-03-09 17:39:44 +00:00
|
|
|
else: # status not in retry_on_status
|
|
|
|
self.logger.info('Job completed with status {}'.format(job.status))
|
2018-03-07 14:39:47 +00:00
|
|
|
if job.status != 'ABORTED':
|
|
|
|
self.context.successful_jobs += 1
|
|
|
|
else:
|
|
|
|
self.context.failed_jobs += 1
|
2018-05-29 11:36:05 +01:00
|
|
|
self.send(signal.JOB_ABORTED)
|
2017-09-18 16:12:03 +01:00
|
|
|
|
|
|
|
def retry_job(self, job):
|
|
|
|
retry_job = Job(job.spec, job.iteration, self.context)
|
|
|
|
retry_job.workload = job.workload
|
|
|
|
retry_job.retries = job.retries + 1
|
|
|
|
retry_job.set_status(Status.PENDING)
|
|
|
|
self.context.job_queue.insert(0, retry_job)
|
2018-05-29 11:36:05 +01:00
|
|
|
self.send(signal.JOB_RESTARTED)
|
2017-10-04 13:24:44 +01:00
|
|
|
|
2017-03-09 14:44:26 +00:00
|
|
|
def send(self, s):
|
|
|
|
signal.send(s, self, self.context)
|
|
|
|
|
2018-03-07 11:32:19 +00:00
|
|
|
def _error_signalled_callback(self, record):
|
|
|
|
self.context.add_event(record.getMessage())
|
|
|
|
|
|
|
|
def _warning_signalled_callback(self, record):
|
|
|
|
self.context.add_event(record.getMessage())
|
|
|
|
|
2017-03-09 14:44:26 +00:00
|
|
|
def __str__(self):
|
|
|
|
return 'runner'
|