926 lines
37 KiB
Python
926 lines
37 KiB
Python
# Lint as: python2, python3
|
|
# Shell class for a test, inherited by all individual tests
|
|
#
|
|
# Methods:
|
|
# __init__ initialise
|
|
# initialize run once for each job
|
|
# setup run once for each new version of the test installed
|
|
# run run the test (wrapped by job.run_test())
|
|
#
|
|
# Data:
|
|
# job backreference to the job this test instance is part of
|
|
# outputdir eg. results/<job>/<testname.tag>
|
|
# resultsdir eg. results/<job>/<testname.tag>/results
|
|
# profdir eg. results/<job>/<testname.tag>/profiling
|
|
# debugdir eg. results/<job>/<testname.tag>/debug
|
|
# bindir eg. tests/<test>
|
|
# src eg. tests/<test>/src
|
|
# tmpdir eg. tmp/<tempname>_<testname.tag>
|
|
|
|
#pylint: disable=C0111
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
|
|
import errno
|
|
import fcntl
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import six
|
|
from six.moves import map
|
|
from six.moves import range
|
|
import stat
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import traceback
|
|
|
|
from autotest_lib.client.bin import utils
|
|
from autotest_lib.client.common_lib import error
|
|
from autotest_lib.client.common_lib import utils as client_utils
|
|
|
|
try:
|
|
from chromite.lib import metrics
|
|
except ImportError:
|
|
metrics = client_utils.metrics_mock
|
|
|
|
|
|
class base_test(object):
|
|
preserve_srcdir = False
|
|
|
|
def __init__(self, job, bindir, outputdir):
|
|
self.job = job
|
|
self.pkgmgr = job.pkgmgr
|
|
self.autodir = job.autodir
|
|
self.outputdir = outputdir
|
|
self.tagged_testname = os.path.basename(self.outputdir)
|
|
self.resultsdir = os.path.join(self.outputdir, 'results')
|
|
os.mkdir(self.resultsdir)
|
|
self.profdir = os.path.join(self.outputdir, 'profiling')
|
|
os.mkdir(self.profdir)
|
|
self.debugdir = os.path.join(self.outputdir, 'debug')
|
|
os.mkdir(self.debugdir)
|
|
# TODO(ericli): figure out how autotest crash handler work with cros
|
|
# Once this is re-enabled import getpass. crosbug.com/31232
|
|
# crash handler, we should restore it in near term.
|
|
# if getpass.getuser() == 'root':
|
|
# self.configure_crash_handler()
|
|
# else:
|
|
self.crash_handling_enabled = False
|
|
self.bindir = bindir
|
|
self.srcdir = os.path.join(self.bindir, 'src')
|
|
self.tmpdir = tempfile.mkdtemp("_" + self.tagged_testname,
|
|
dir=job.tmpdir)
|
|
# The crash_reporter uses this file to determine which test is in
|
|
# progress.
|
|
self.test_in_prog_file = '/run/crash_reporter/test-in-prog'
|
|
self._keyvals = []
|
|
self._new_keyval = False
|
|
self.failed_constraints = []
|
|
self.iteration = 0
|
|
self.before_iteration_hooks = []
|
|
self.after_iteration_hooks = []
|
|
|
|
# Flag to indicate if the test has succeeded or failed.
|
|
self.success = False
|
|
|
|
|
|
def configure_crash_handler(self):
|
|
pass
|
|
|
|
|
|
def crash_handler_report(self):
|
|
pass
|
|
|
|
|
|
def assert_(self, expr, msg='Assertion failed.'):
|
|
if not expr:
|
|
raise error.TestError(msg)
|
|
|
|
|
|
def write_test_keyval(self, attr_dict):
|
|
utils.write_keyval(self.outputdir, attr_dict)
|
|
|
|
|
|
@staticmethod
|
|
def _append_type_to_keys(dictionary, typename):
|
|
new_dict = {}
|
|
for key, value in six.iteritems(dictionary):
|
|
new_key = "%s{%s}" % (key, typename)
|
|
new_dict[new_key] = value
|
|
return new_dict
|
|
|
|
def output_perf_value(self, description, value, units=None,
|
|
higher_is_better=None, graph=None,
|
|
replacement='_', replace_existing_values=False,
|
|
resultsdir=None):
|
|
"""
|
|
Records a measured performance value in an output file.
|
|
|
|
The output file will subsequently be parsed by the TKO parser to have
|
|
the information inserted into the results database.
|
|
|
|
@param description: A string describing the measured perf value. Must
|
|
be maximum length 256, and may only contain letters, numbers,
|
|
periods, dashes, and underscores. For example:
|
|
"page_load_time", "scrolling-frame-rate".
|
|
@param value: A number representing the measured perf value, or a list
|
|
of measured values if a test takes multiple measurements.
|
|
Measured perf values can be either ints or floats.
|
|
@param units: A string describing the units associated with the
|
|
measured perf value. Must be maximum length 32, and may only
|
|
contain letters, numbers, periods, dashes, and underscores.
|
|
For example: "msec", "fps", "score", "runs_per_second".
|
|
@param higher_is_better: A boolean indicating whether or not a "higher"
|
|
measured perf value is considered to be better. If False, it is
|
|
assumed that a "lower" measured value is considered to be
|
|
better. This impacts dashboard plotting and email notification.
|
|
Pure autotests are expected to specify either True or False!
|
|
This value can be set to "None" to indicate that the perf
|
|
dashboard should apply the rules encoded via Chromium
|
|
unit-info.json. This is only used for tracking Chromium based
|
|
tests (in particular telemetry).
|
|
@param graph: A string indicating the name of the graph on which
|
|
the perf value will be subsequently displayed on the chrome perf
|
|
dashboard. This allows multiple metrics be grouped together on
|
|
the same graphs. Defaults to None, indicating that the perf
|
|
value should be displayed individually on a separate graph.
|
|
@param replacement: string to replace illegal characters in
|
|
|description| and |units| with.
|
|
@param replace_existing_values: A boolean indicating whether or not a
|
|
new added perf value should replace existing perf.
|
|
@param resultsdir: An optional path to specify a custom output
|
|
directory.
|
|
"""
|
|
if len(description) > 256:
|
|
raise ValueError('The description must be at most 256 characters.')
|
|
if units and len(units) > 32:
|
|
raise ValueError('The units must be at most 32 characters.')
|
|
|
|
# If |replacement| is legal replace illegal characters with it.
|
|
string_regex = re.compile(r'[^-\.\w]')
|
|
if replacement is None or re.search(string_regex, replacement):
|
|
raise ValueError('Invalid replacement string to mask illegal '
|
|
'characters. May only contain letters, numbers, '
|
|
'periods, dashes, and underscores. '
|
|
'replacement: %s' % replacement)
|
|
description = re.sub(string_regex, replacement, description)
|
|
units = re.sub(string_regex, replacement, units) if units else None
|
|
|
|
charts = {}
|
|
if not resultsdir:
|
|
resultsdir = self.resultsdir
|
|
if not os.path.exists(resultsdir):
|
|
os.makedirs(resultsdir)
|
|
output_file = os.path.join(resultsdir, 'results-chart.json')
|
|
if os.path.isfile(output_file):
|
|
with open(output_file, 'r') as fp:
|
|
contents = fp.read()
|
|
if contents:
|
|
charts = json.loads(contents)
|
|
|
|
if graph:
|
|
first_level = graph
|
|
second_level = description
|
|
else:
|
|
first_level = description
|
|
second_level = 'summary'
|
|
|
|
direction = 'up' if higher_is_better else 'down'
|
|
|
|
# All input should be a number - but at times there are strings
|
|
# representing numbers logged, attempt to convert them to numbers.
|
|
# If a non number string is logged an exception will be thrown.
|
|
if isinstance(value, list):
|
|
value = list(map(float, value))
|
|
else:
|
|
value = float(value)
|
|
|
|
result_type = 'scalar'
|
|
value_key = 'value'
|
|
result_value = value
|
|
|
|
# The chart json spec go/telemetry-json differenciates between a single
|
|
# value vs a list of values. Lists of values get extra processing in
|
|
# the chromeperf dashboard ( mean, standard deviation etc)
|
|
# Tests can log one or more values for the same metric, to adhere stricly
|
|
# to the specification the first value logged is a scalar but if another
|
|
# value is logged the results become a list of scalar.
|
|
# TODO Figure out if there would be any difference of always using list
|
|
# of scalar even if there is just one item in the list.
|
|
if isinstance(value, list):
|
|
result_type = 'list_of_scalar_values'
|
|
value_key = 'values'
|
|
if first_level in charts and second_level in charts[first_level]:
|
|
if 'values' in charts[first_level][second_level]:
|
|
result_value = charts[first_level][second_level]['values']
|
|
elif 'value' in charts[first_level][second_level]:
|
|
result_value = [charts[first_level][second_level]['value']]
|
|
if replace_existing_values:
|
|
result_value = value
|
|
else:
|
|
result_value.extend(value)
|
|
else:
|
|
result_value = value
|
|
elif (first_level in charts and second_level in charts[first_level] and
|
|
not replace_existing_values):
|
|
result_type = 'list_of_scalar_values'
|
|
value_key = 'values'
|
|
if 'values' in charts[first_level][second_level]:
|
|
result_value = charts[first_level][second_level]['values']
|
|
result_value.append(value)
|
|
else:
|
|
result_value = [charts[first_level][second_level]['value'], value]
|
|
|
|
test_data = {
|
|
second_level: {
|
|
'type': result_type,
|
|
'units': units,
|
|
value_key: result_value,
|
|
'improvement_direction': direction
|
|
}
|
|
}
|
|
|
|
if first_level in charts:
|
|
charts[first_level].update(test_data)
|
|
else:
|
|
charts.update({first_level: test_data})
|
|
|
|
with open(output_file, 'w') as fp:
|
|
fp.write(json.dumps(charts, indent=2))
|
|
|
|
|
|
def write_perf_keyval(self, perf_dict):
|
|
self.write_iteration_keyval({}, perf_dict)
|
|
|
|
|
|
def write_attr_keyval(self, attr_dict):
|
|
self.write_iteration_keyval(attr_dict, {})
|
|
|
|
|
|
def write_iteration_keyval(self, attr_dict, perf_dict):
|
|
# append the dictionaries before they have the {perf} and {attr} added
|
|
self._keyvals.append({'attr':attr_dict, 'perf':perf_dict})
|
|
self._new_keyval = True
|
|
|
|
if attr_dict:
|
|
attr_dict = self._append_type_to_keys(attr_dict, "attr")
|
|
utils.write_keyval(self.resultsdir, attr_dict, type_tag="attr")
|
|
|
|
if perf_dict:
|
|
perf_dict = self._append_type_to_keys(perf_dict, "perf")
|
|
utils.write_keyval(self.resultsdir, perf_dict, type_tag="perf")
|
|
|
|
keyval_path = os.path.join(self.resultsdir, "keyval")
|
|
print("", file=open(keyval_path, "a"))
|
|
|
|
|
|
def analyze_perf_constraints(self, constraints):
|
|
if not self._new_keyval:
|
|
return
|
|
|
|
# create a dict from the keyvals suitable as an environment for eval
|
|
keyval_env = self._keyvals[-1]['perf'].copy()
|
|
keyval_env['__builtins__'] = None
|
|
self._new_keyval = False
|
|
failures = []
|
|
|
|
# evaluate each constraint using the current keyvals
|
|
for constraint in constraints:
|
|
logging.info('___________________ constraint = %s', constraint)
|
|
logging.info('___________________ keyvals = %s', keyval_env)
|
|
|
|
try:
|
|
if not eval(constraint, keyval_env):
|
|
failures.append('%s: constraint was not met' % constraint)
|
|
except:
|
|
failures.append('could not evaluate constraint: %s'
|
|
% constraint)
|
|
|
|
# keep track of the errors for each iteration
|
|
self.failed_constraints.append(failures)
|
|
|
|
|
|
def process_failed_constraints(self):
|
|
msg = ''
|
|
for i, failures in enumerate(self.failed_constraints):
|
|
if failures:
|
|
msg += 'iteration %d:%s ' % (i, ','.join(failures))
|
|
|
|
if msg:
|
|
raise error.TestFail(msg)
|
|
|
|
|
|
def register_before_iteration_hook(self, iteration_hook):
|
|
"""
|
|
This is how we expect test writers to register a before_iteration_hook.
|
|
This adds the method to the list of hooks which are executed
|
|
before each iteration.
|
|
|
|
@param iteration_hook: Method to run before each iteration. A valid
|
|
hook accepts a single argument which is the
|
|
test object.
|
|
"""
|
|
self.before_iteration_hooks.append(iteration_hook)
|
|
|
|
|
|
def register_after_iteration_hook(self, iteration_hook):
|
|
"""
|
|
This is how we expect test writers to register an after_iteration_hook.
|
|
This adds the method to the list of hooks which are executed
|
|
after each iteration. Hooks are executed starting with the most-
|
|
recently registered, in stack fashion.
|
|
|
|
@param iteration_hook: Method to run after each iteration. A valid
|
|
hook accepts a single argument which is the
|
|
test object.
|
|
"""
|
|
self.after_iteration_hooks.append(iteration_hook)
|
|
|
|
|
|
def initialize(self):
|
|
pass
|
|
|
|
|
|
def setup(self):
|
|
pass
|
|
|
|
|
|
def warmup(self, *args, **dargs):
|
|
pass
|
|
|
|
|
|
def drop_caches_between_iterations(self):
|
|
if self.job.drop_caches_between_iterations:
|
|
utils.drop_caches()
|
|
|
|
|
|
def _call_run_once(self, constraints, profile_only,
|
|
postprocess_profiled_run, args, dargs):
|
|
self.drop_caches_between_iterations()
|
|
# execute iteration hooks
|
|
if not self.job.fast:
|
|
logging.debug('Starting before_iteration_hooks for %s',
|
|
self.tagged_testname)
|
|
with metrics.SecondsTimer(
|
|
'chromeos/autotest/job/before_iteration_hook_duration'):
|
|
for hook in self.before_iteration_hooks:
|
|
hook(self)
|
|
logging.debug('before_iteration_hooks completed')
|
|
|
|
finished = False
|
|
|
|
# Mark the current test in progress so that crash_reporter can report
|
|
# it in uploaded crashes.
|
|
# if the file already exists, truncate and overwrite.
|
|
# TODO(mutexlox): Determine what to do if the file already exists, which
|
|
# could happen for a few reasons:
|
|
# * An earlier tast or autotest run crashed before removing the
|
|
# test-in-prog file. In this case, we would ideally overwrite the
|
|
# existing test name and _not_ restore it.
|
|
# * An autotest ran another autotest (e.g. logging_GenerateCrashFiles
|
|
# runs desktopui_SimpleLogin). In this case, arguably it makes sense
|
|
# to attribute the crash to logging_GenerateCrashFiles and not to
|
|
# desktopui_SimpleLogin (or to attribute it to both), since the
|
|
# context in which it's running is different than if it were run on
|
|
# its own.
|
|
# * Every tast test is kicked off by the 'tast' autotest (see
|
|
# server/site_tests/tast/tast.py), so the file will always be
|
|
# populated when the tast suite starts running. In this case, we
|
|
# want to attribute crashes that happen during a specific test to
|
|
# that test, but if the tast infra causes a crash we should
|
|
# attribute the crash to it (e.g. to the 'tast.critical-system'
|
|
# "autotest"). For this case, we should save the contents of the
|
|
# file before a test and restore it after.
|
|
if ('host' in dargs and hasattr(dargs['host'], 'is_up_fast') and
|
|
dargs['host'].is_up_fast()):
|
|
dargs['host'].run('echo %s > %s' %
|
|
(self.tagged_testname, self.test_in_prog_file),
|
|
ignore_status=True)
|
|
else:
|
|
crash_run_dir = os.path.dirname(self.test_in_prog_file)
|
|
try:
|
|
# Only try to create the file if the directory already exists
|
|
# (otherwise, we may not be on a CrOS device)
|
|
if os.path.exists(crash_run_dir):
|
|
with open(self.test_in_prog_file, 'w') as f:
|
|
f.write(self.tagged_testname)
|
|
except: # Broad 'except' because we don't want this to block tests
|
|
logging.warning('failed to write in progress test name')
|
|
try:
|
|
if profile_only:
|
|
if not self.job.profilers.present():
|
|
self.job.record('WARN', None, None,
|
|
'No profilers have been added but '
|
|
'profile_only is set - nothing '
|
|
'will be run')
|
|
self.run_once_profiling(postprocess_profiled_run,
|
|
*args, **dargs)
|
|
else:
|
|
self.before_run_once()
|
|
logging.debug('starting test(run_once()), test details follow'
|
|
'\nargs: %r\nkwargs: %r', args, dargs)
|
|
self.run_once(*args, **dargs)
|
|
logging.debug('The test has completed successfully')
|
|
self.after_run_once()
|
|
|
|
self.postprocess_iteration()
|
|
self.analyze_perf_constraints(constraints)
|
|
finished = True
|
|
# Catch and re-raise to let after_iteration_hooks see the exception.
|
|
except Exception as e:
|
|
logging.debug('Test failed due to %s. Exception log follows the '
|
|
'after_iteration_hooks.', str(e))
|
|
raise
|
|
finally:
|
|
if ('host' in dargs and hasattr(dargs['host'], 'is_up_fast') and
|
|
dargs['host'].is_up_fast()):
|
|
dargs['host'].run('rm -f %s' % self.test_in_prog_file)
|
|
else:
|
|
try:
|
|
# Unmark the test as running.
|
|
os.remove(self.test_in_prog_file)
|
|
except OSError as e:
|
|
# If something removed it, do nothing--we're in the desired
|
|
# state (the file is gone). Otherwise, log.
|
|
if e.errno != errno.ENOENT:
|
|
logging.warning(
|
|
"Couldn't remove test-in-prog file: %s",
|
|
traceback.format_exc())
|
|
|
|
if not finished or not self.job.fast:
|
|
logging.debug('Starting after_iteration_hooks for %s',
|
|
self.tagged_testname)
|
|
with metrics.SecondsTimer(
|
|
'chromeos/autotest/job/after_iteration_hook_duration'):
|
|
for hook in reversed(self.after_iteration_hooks):
|
|
hook(self)
|
|
logging.debug('after_iteration_hooks completed')
|
|
|
|
|
|
def execute(self, iterations=None, test_length=None, profile_only=None,
|
|
_get_time=time.time, postprocess_profiled_run=None,
|
|
constraints=(), *args, **dargs):
|
|
"""
|
|
This is the basic execute method for the tests inherited from base_test.
|
|
If you want to implement a benchmark test, it's better to implement
|
|
the run_once function, to cope with the profiling infrastructure. For
|
|
other tests, you can just override the default implementation.
|
|
|
|
@param test_length: The minimum test length in seconds. We'll run the
|
|
run_once function for a number of times large enough to cover the
|
|
minimum test length.
|
|
|
|
@param iterations: A number of iterations that we'll run the run_once
|
|
function. This parameter is incompatible with test_length and will
|
|
be silently ignored if you specify both.
|
|
|
|
@param profile_only: If true run X iterations with profilers enabled.
|
|
If false run X iterations and one with profiling if profiles are
|
|
enabled. If None, default to the value of job.default_profile_only.
|
|
|
|
@param _get_time: [time.time] Used for unit test time injection.
|
|
|
|
@param postprocess_profiled_run: Run the postprocessing for the
|
|
profiled run.
|
|
"""
|
|
|
|
# For our special class of tests, the benchmarks, we don't want
|
|
# profilers to run during the test iterations. Let's reserve only
|
|
# the last iteration for profiling, if needed. So let's stop
|
|
# all profilers if they are present and active.
|
|
profilers = self.job.profilers
|
|
if profilers.active():
|
|
profilers.stop(self)
|
|
if profile_only is None:
|
|
profile_only = self.job.default_profile_only
|
|
# If the user called this test in an odd way (specified both iterations
|
|
# and test_length), let's warn them.
|
|
if iterations and test_length:
|
|
logging.debug('Iterations parameter ignored (timed execution)')
|
|
if test_length:
|
|
test_start = _get_time()
|
|
time_elapsed = 0
|
|
timed_counter = 0
|
|
logging.debug('Test started. Specified %d s as the minimum test '
|
|
'length', test_length)
|
|
while time_elapsed < test_length:
|
|
timed_counter = timed_counter + 1
|
|
if time_elapsed == 0:
|
|
logging.debug('Executing iteration %d', timed_counter)
|
|
elif time_elapsed > 0:
|
|
logging.debug('Executing iteration %d, time_elapsed %d s',
|
|
timed_counter, time_elapsed)
|
|
self._call_run_once(constraints, profile_only,
|
|
postprocess_profiled_run, args, dargs)
|
|
test_iteration_finish = _get_time()
|
|
time_elapsed = test_iteration_finish - test_start
|
|
logging.debug('Test finished after %d iterations, '
|
|
'time elapsed: %d s', timed_counter, time_elapsed)
|
|
else:
|
|
if iterations is None:
|
|
iterations = 1
|
|
if iterations > 1:
|
|
logging.debug('Test started. Specified %d iterations',
|
|
iterations)
|
|
for self.iteration in range(1, iterations + 1):
|
|
if iterations > 1:
|
|
logging.debug('Executing iteration %d of %d',
|
|
self.iteration, iterations)
|
|
self._call_run_once(constraints, profile_only,
|
|
postprocess_profiled_run, args, dargs)
|
|
|
|
if not profile_only:
|
|
self.iteration += 1
|
|
self.run_once_profiling(postprocess_profiled_run, *args, **dargs)
|
|
|
|
# Do any postprocessing, normally extracting performance keyvals, etc
|
|
self.postprocess()
|
|
self.process_failed_constraints()
|
|
|
|
|
|
def run_once_profiling(self, postprocess_profiled_run, *args, **dargs):
|
|
profilers = self.job.profilers
|
|
# Do a profiling run if necessary
|
|
if profilers.present():
|
|
self.drop_caches_between_iterations()
|
|
profilers.before_start(self)
|
|
|
|
self.before_run_once()
|
|
profilers.start(self)
|
|
logging.debug('Profilers present. Profiling run started')
|
|
|
|
try:
|
|
self.run_once(*args, **dargs)
|
|
|
|
# Priority to the run_once() argument over the attribute.
|
|
postprocess_attribute = getattr(self,
|
|
'postprocess_profiled_run',
|
|
False)
|
|
|
|
if (postprocess_profiled_run or
|
|
(postprocess_profiled_run is None and
|
|
postprocess_attribute)):
|
|
self.postprocess_iteration()
|
|
|
|
finally:
|
|
profilers.stop(self)
|
|
profilers.report(self)
|
|
|
|
self.after_run_once()
|
|
|
|
|
|
def postprocess(self):
|
|
pass
|
|
|
|
|
|
def postprocess_iteration(self):
|
|
pass
|
|
|
|
|
|
def cleanup(self):
|
|
pass
|
|
|
|
|
|
def before_run_once(self):
|
|
"""
|
|
Override in tests that need it, will be called before any run_once()
|
|
call including the profiling run (when it's called before starting
|
|
the profilers).
|
|
"""
|
|
pass
|
|
|
|
|
|
def after_run_once(self):
|
|
"""
|
|
Called after every run_once (including from a profiled run when it's
|
|
called after stopping the profilers).
|
|
"""
|
|
pass
|
|
|
|
|
|
@staticmethod
|
|
def _make_writable_to_others(directory):
|
|
mode = os.stat(directory).st_mode
|
|
mode = mode | stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH
|
|
os.chmod(directory, mode)
|
|
|
|
|
|
def _exec(self, args, dargs):
|
|
self.job.logging.tee_redirect_debug_dir(self.debugdir,
|
|
log_name=self.tagged_testname)
|
|
try:
|
|
# write out the test attributes into a keyval
|
|
dargs = dargs.copy()
|
|
run_cleanup = dargs.pop('run_cleanup', self.job.run_test_cleanup)
|
|
keyvals = dargs.pop('test_attributes', {}).copy()
|
|
keyvals['version'] = self.version
|
|
for i, arg in enumerate(args):
|
|
keyvals['param-%d' % i] = repr(arg)
|
|
for name, arg in six.iteritems(dargs):
|
|
keyvals['param-%s' % name] = repr(arg)
|
|
self.write_test_keyval(keyvals)
|
|
|
|
_validate_args(args, dargs, self.initialize, self.setup,
|
|
self.execute, self.cleanup)
|
|
|
|
try:
|
|
# Make resultsdir and tmpdir accessible to everyone. We may
|
|
# output data to these directories as others, e.g., chronos.
|
|
self._make_writable_to_others(self.tmpdir)
|
|
self._make_writable_to_others(self.resultsdir)
|
|
|
|
# Initialize:
|
|
utils.cherry_pick_call(self.initialize, *args, **dargs)
|
|
|
|
lockfile = open(os.path.join(self.job.tmpdir, '.testlock'), 'w')
|
|
try:
|
|
fcntl.flock(lockfile, fcntl.LOCK_EX)
|
|
# Setup: (compile and install the test, if needed)
|
|
p_args, p_dargs = utils.cherry_pick_args(self.setup, args, dargs)
|
|
utils.update_version(self.srcdir, self.preserve_srcdir,
|
|
self.version, self.setup,
|
|
*p_args, **p_dargs)
|
|
finally:
|
|
fcntl.flock(lockfile, fcntl.LOCK_UN)
|
|
lockfile.close()
|
|
|
|
# Execute:
|
|
os.chdir(self.outputdir)
|
|
|
|
# call self.warmup cherry picking the arguments it accepts and
|
|
# translate exceptions if needed
|
|
_call_test_function(utils.cherry_pick_call, self.warmup,
|
|
*args, **dargs)
|
|
|
|
if hasattr(self, 'run_once'):
|
|
p_args, p_dargs = utils.cherry_pick_args(self.run_once,
|
|
args, dargs)
|
|
# pull in any non-* and non-** args from self.execute
|
|
for param in utils.get_nonstar_args(self.execute):
|
|
if param in dargs:
|
|
p_dargs[param] = dargs[param]
|
|
else:
|
|
p_args, p_dargs = utils.cherry_pick_args(self.execute,
|
|
args, dargs)
|
|
|
|
_call_test_function(self.execute, *p_args, **p_dargs)
|
|
except Exception:
|
|
# Save the exception while we run our cleanup() before
|
|
# reraising it, but log it to so actual time of error is known.
|
|
exc_info = sys.exc_info()
|
|
logging.warning('The test failed with the following exception',
|
|
exc_info=True)
|
|
|
|
try:
|
|
try:
|
|
if run_cleanup:
|
|
logging.debug('Running cleanup for test.')
|
|
utils.cherry_pick_call(self.cleanup, *args, **dargs)
|
|
except Exception:
|
|
logging.error('Ignoring exception during cleanup() '
|
|
'phase:')
|
|
traceback.print_exc()
|
|
logging.error('Now raising the earlier %s error',
|
|
exc_info[0])
|
|
self.crash_handler_report()
|
|
finally:
|
|
# Raise exception after running cleanup, reporting crash,
|
|
# and restoring job's logging, even if the first two
|
|
# actions fail.
|
|
self.job.logging.restore()
|
|
try:
|
|
six.reraise(exc_info[0], exc_info[1], exc_info[2])
|
|
finally:
|
|
# http://docs.python.org/library/sys.html#sys.exc_info
|
|
# Be nice and prevent a circular reference.
|
|
del exc_info
|
|
else:
|
|
try:
|
|
if run_cleanup:
|
|
utils.cherry_pick_call(self.cleanup, *args, **dargs)
|
|
self.crash_handler_report()
|
|
finally:
|
|
self.job.logging.restore()
|
|
except error.AutotestError:
|
|
# Pass already-categorized errors on up.
|
|
raise
|
|
except Exception as e:
|
|
# Anything else is an ERROR in our own code, not execute().
|
|
raise error.UnhandledTestError(e)
|
|
|
|
def runsubtest(self, url, *args, **dargs):
|
|
"""
|
|
Execute another autotest test from inside the current test's scope.
|
|
|
|
@param test: Parent test.
|
|
@param url: Url of new test.
|
|
@param tag: Tag added to test name.
|
|
@param args: Args for subtest.
|
|
@param dargs: Dictionary with args for subtest.
|
|
@iterations: Number of subtest iterations.
|
|
@profile_only: If true execute one profiled run.
|
|
"""
|
|
dargs["profile_only"] = dargs.get("profile_only", False)
|
|
test_basepath = self.outputdir[len(self.job.resultdir + "/"):]
|
|
return self.job.run_test(url, main_testpath=test_basepath,
|
|
*args, **dargs)
|
|
|
|
|
|
def _validate_args(args, dargs, *funcs):
|
|
"""Verify that arguments are appropriate for at least one callable.
|
|
|
|
Given a list of callables as additional parameters, verify that
|
|
the proposed keyword arguments in dargs will each be accepted by at least
|
|
one of the callables.
|
|
|
|
NOTE: args is currently not supported and must be empty.
|
|
|
|
Args:
|
|
args: A tuple of proposed positional arguments.
|
|
dargs: A dictionary of proposed keyword arguments.
|
|
*funcs: Callables to be searched for acceptance of args and dargs.
|
|
Raises:
|
|
error.AutotestError: if an arg won't be accepted by any of *funcs.
|
|
"""
|
|
all_co_flags = 0
|
|
all_varnames = ()
|
|
for func in funcs:
|
|
all_co_flags |= func.__code__.co_flags
|
|
all_varnames += func.__code__.co_varnames[:func.__code__.co_argcount]
|
|
|
|
# Check if given args belongs to at least one of the methods below.
|
|
if len(args) > 0:
|
|
# Current implementation doesn't allow the use of args.
|
|
raise error.TestError('Unnamed arguments not accepted. Please '
|
|
'call job.run_test with named args only')
|
|
|
|
# Check if given dargs belongs to at least one of the methods below.
|
|
if len(dargs) > 0:
|
|
if not all_co_flags & 0x08:
|
|
# no func accepts *dargs, so:
|
|
for param in dargs:
|
|
if not param in all_varnames:
|
|
raise error.AutotestError('Unknown parameter: %s' % param)
|
|
|
|
|
|
def _installtest(job, url):
|
|
(group, name) = job.pkgmgr.get_package_name(url, 'test')
|
|
|
|
# Bail if the test is already installed
|
|
group_dir = os.path.join(job.testdir, "download", group)
|
|
if os.path.exists(os.path.join(group_dir, name)):
|
|
return (group, name)
|
|
|
|
# If the group directory is missing create it and add
|
|
# an empty __init__.py so that sub-directories are
|
|
# considered for import.
|
|
if not os.path.exists(group_dir):
|
|
os.makedirs(group_dir)
|
|
f = open(os.path.join(group_dir, '__init__.py'), 'w+')
|
|
f.close()
|
|
|
|
logging.debug("%s: installing test url=%s", name, url)
|
|
tarball = os.path.basename(url)
|
|
tarball_path = os.path.join(group_dir, tarball)
|
|
test_dir = os.path.join(group_dir, name)
|
|
job.pkgmgr.fetch_pkg(tarball, tarball_path,
|
|
repo_url = os.path.dirname(url))
|
|
|
|
# Create the directory for the test
|
|
if not os.path.exists(test_dir):
|
|
os.mkdir(os.path.join(group_dir, name))
|
|
|
|
job.pkgmgr.untar_pkg(tarball_path, test_dir)
|
|
|
|
os.remove(tarball_path)
|
|
|
|
# For this 'sub-object' to be importable via the name
|
|
# 'group.name' we need to provide an __init__.py,
|
|
# so link the main entry point to this.
|
|
os.symlink(name + '.py', os.path.join(group_dir, name,
|
|
'__init__.py'))
|
|
|
|
# The test is now installed.
|
|
return (group, name)
|
|
|
|
|
|
def _call_test_function(func, *args, **dargs):
|
|
"""Calls a test function and translates exceptions so that errors
|
|
inside test code are considered test failures."""
|
|
try:
|
|
return func(*args, **dargs)
|
|
except error.AutotestError:
|
|
raise
|
|
except Exception as e:
|
|
# Other exceptions must be treated as a FAIL when
|
|
# raised during the test functions
|
|
raise error.UnhandledTestFail(e)
|
|
|
|
|
|
def runtest(job,
|
|
url,
|
|
tag,
|
|
args,
|
|
dargs,
|
|
local_namespace={},
|
|
global_namespace={},
|
|
before_test_hook=None,
|
|
after_test_hook=None,
|
|
before_iteration_hook=None,
|
|
after_iteration_hook=None,
|
|
override_test_in_prog_file=None):
|
|
local_namespace = local_namespace.copy()
|
|
global_namespace = global_namespace.copy()
|
|
# if this is not a plain test name then download and install the
|
|
# specified test
|
|
if url.endswith('.tar.bz2'):
|
|
(testgroup, testname) = _installtest(job, url)
|
|
bindir = os.path.join(job.testdir, 'download', testgroup, testname)
|
|
importdir = os.path.join(job.testdir, 'download')
|
|
modulename = '%s.%s' % (re.sub('/', '.', testgroup), testname)
|
|
classname = '%s.%s' % (modulename, testname)
|
|
path = testname
|
|
else:
|
|
# If the test is local, it may be under either testdir or site_testdir.
|
|
# Tests in site_testdir override tests defined in testdir
|
|
testname = path = url
|
|
testgroup = ''
|
|
path = re.sub(':', '/', testname)
|
|
modulename = os.path.basename(path)
|
|
classname = '%s.%s' % (modulename, modulename)
|
|
|
|
# Try installing the test package
|
|
# The job object may be either a server side job or a client side job.
|
|
# 'install_pkg' method will be present only if it's a client side job.
|
|
if hasattr(job, 'install_pkg'):
|
|
try:
|
|
bindir = os.path.join(job.testdir, testname)
|
|
job.install_pkg(testname, 'test', bindir)
|
|
except error.PackageInstallError:
|
|
# continue as a fall back mechanism and see if the test code
|
|
# already exists on the machine
|
|
pass
|
|
|
|
bindir = None
|
|
for dir in [job.testdir, getattr(job, 'site_testdir', None)]:
|
|
if dir is not None and os.path.exists(os.path.join(dir, path)):
|
|
importdir = bindir = os.path.join(dir, path)
|
|
if not bindir:
|
|
raise error.TestError(testname + ': test does not exist')
|
|
|
|
subdir = os.path.join(dargs.pop('main_testpath', ""), testname)
|
|
outputdir = os.path.join(job.resultdir, subdir)
|
|
if tag:
|
|
outputdir += '.' + tag
|
|
|
|
local_namespace['job'] = job
|
|
local_namespace['bindir'] = bindir
|
|
local_namespace['outputdir'] = outputdir
|
|
|
|
sys.path.insert(0, importdir)
|
|
try:
|
|
exec ('import %s' % modulename, local_namespace, global_namespace)
|
|
exec ("mytest = %s(job, bindir, outputdir)" % classname,
|
|
local_namespace, global_namespace)
|
|
finally:
|
|
sys.path.pop(0)
|
|
|
|
pwd = os.getcwd()
|
|
os.chdir(outputdir)
|
|
|
|
try:
|
|
mytest = global_namespace['mytest']
|
|
if override_test_in_prog_file:
|
|
mytest.test_in_prog_file = override_test_in_prog_file
|
|
mytest.success = False
|
|
if not job.fast and before_test_hook:
|
|
logging.info('Starting before_hook for %s', mytest.tagged_testname)
|
|
with metrics.SecondsTimer(
|
|
'chromeos/autotest/job/before_hook_duration'):
|
|
before_test_hook(mytest)
|
|
logging.info('before_hook completed')
|
|
|
|
# we use the register iteration hooks methods to register the passed
|
|
# in hooks
|
|
if before_iteration_hook:
|
|
mytest.register_before_iteration_hook(before_iteration_hook)
|
|
if after_iteration_hook:
|
|
mytest.register_after_iteration_hook(after_iteration_hook)
|
|
mytest._exec(args, dargs)
|
|
mytest.success = True
|
|
finally:
|
|
os.chdir(pwd)
|
|
if after_test_hook and (not mytest.success or not job.fast):
|
|
logging.info('Starting after_hook for %s', mytest.tagged_testname)
|
|
with metrics.SecondsTimer(
|
|
'chromeos/autotest/job/after_hook_duration'):
|
|
after_test_hook(mytest)
|
|
logging.info('after_hook completed')
|
|
|
|
shutil.rmtree(mytest.tmpdir, ignore_errors=True)
|