blob: 406cd49d60be0a69aa66cc050cb54e972132c769 [file] [log] [blame]
# Copyright 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import bz2
import glob
import json
import logging
import os
import re
import shutil
import tempfile
import time
import xml.etree.ElementTree as et
from autotest_lib.client.bin import test, utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.cros import cros_logging, service_stopper
from import graphics_utils
RERUN_RATIO = 0.02 # Ratio to rerun failing test for hasty mode
class graphics_dEQP(graphics_utils.GraphicsTest):
"""Run the drawElements Quality Program test suite.
version = 1
_services = None
_hasty = False
_hasty_batch_size = 100 # Batch size in hasty mode.
_shard_number = 0
_shard_count = 1
_board = None
_cpu_type = None
_gpu_type = None
_surface = None
_filter = None
_width = 256 # Use smallest width for which all tests run/pass.
_height = 256 # Use smallest height for which all tests run/pass.
_timeout = 70 # Larger than twice the dEQP watchdog timeout at 30s.
_test_names = None
_test_names_file = None
_log_path = None # Location for detailed test output logs (in /tmp/).
_debug = False # Analyze kernel messages.
_log_reader = None # Reader to analyze (kernel) messages log.
_log_filter = re.compile('.* .* kernel:') # kernel messages filter.
_env = None # environment for test processes
'dEQP-EGL': 'egl',
'dEQP-GLES2': 'gles2',
'dEQP-GLES3': 'gles3',
'dEQP-GLES31': 'gles31',
'dEQP-VK': 'vk',
# We do not consider these results as failures.
'pass', 'notsupported', 'internalerror', 'qualitywarning',
'compatibilitywarning', 'skipped'
def initialize(self):
super(graphics_dEQP, self).initialize()
self._api_helper = graphics_utils.GraphicsApiHelper()
self._board = utils.get_board()
self._cpu_type = utils.get_cpu_soc_family()
self._gpu_type = utils.get_gpu_family()
# deqp may depend on libraries that are present only on test images.
# Those libraries are installed in /usr/local.
self._env = os.environ.copy()
old_ld_path = self._env.get('LD_LIBRARY_PATH', '')
if old_ld_path:
self._env['LD_LIBRARY_PATH'] = '/usr/local/lib:/usr/local/lib64:' + old_ld_path
self._env['LD_LIBRARY_PATH'] = '/usr/local/lib:/usr/local/lib64'
self._services = service_stopper.ServiceStopper(['ui', 'powerd'])
# Valid choices are fbo and pbuffer. The latter avoids dEQP assumptions.
self._surface = 'pbuffer'
def cleanup(self):
if self._services:
super(graphics_dEQP, self).cleanup()
def _parse_test_results(self, result_filename,
test_results=None, failing_test=None):
"""Handles result files with one or more test results.
@param result_filename: log file to parse.
@param test_results: Result parsed will be appended to it.
@param failing_test: Tests considered failed will append to it.
@return: dictionary of parsed test results.
xml = ''
xml_start = False
xml_complete = False
xml_bad = False
result = 'ParseTestResultFail'
if test_results is None:
test_results = {}
if not os.path.isfile(result_filename):
return test_results
with open(result_filename) as result_file:
for line in result_file.readlines():
# If the test terminates early, the XML will be incomplete
# and should not be parsed.
if line.startswith('#terminateTestCaseResult'):
result = line.strip().split()[1]
xml_bad = True
# Will only see #endTestCaseResult if the test does not
# terminate early.
elif line.startswith('#endTestCaseResult'):
xml_complete = True
elif xml_start:
xml += line
elif line.startswith('#beginTestCaseResult'):
# If we see another begin before an end then something is
# wrong.
if xml_start:
xml_bad = True
xml_start = True
test_case = line.split(' ')[1]
if xml_complete or xml_bad:
if xml_complete:
myparser = et.XMLParser(encoding='ISO-8859-1')
root = et.fromstring(xml, parser=myparser)
test_case = root.attrib['CasePath']
result = root.find('Result').get('StatusCode').strip()
xml_complete = False
test_results[result] = test_results.get(result, 0) + 1
if (result.lower() not in self.TEST_RESULT_FILTER and
failing_test != None):
xml_bad = False
xml_start = False
result = 'ParseTestResultFail'
xml = ''
return test_results
def _load_not_passing_cases(self, test_filter):
"""Load all test cases that are in non-'Pass' expectations."""
not_passing_cases = []
expectations_dir = os.path.join(self.bindir, 'expectations',
subset_spec = '%s.*' % test_filter
subset_paths = glob.glob(os.path.join(expectations_dir, subset_spec))
for subset_file in subset_paths:
# Filter against extra hasty failures only in hasty mode.
if (not '.Pass.bz2' in subset_file and
(self._hasty or '.hasty.' not in subset_file)):
return not_passing_cases
def _translate_name_to_api(self, name):
"""Translate test_names or test_filter to api."""
test_prefix = name.split('.')[0]
if test_prefix in self.DEQP_MODULES:
api = self.DEQP_MODULES[test_prefix]
raise error.TestFail('Failed: Invalid test name: %s' % name)
return api
def _get_executable(self, api):
"""Return the executable path of the api."""
return self._api_helper.get_deqp_executable(api)
def _can_run(self, api):
"""Check if specific api is supported in this board."""
return api in self._api_helper.get_supported_apis()
def _bootstrap_new_test_cases(self, test_filter):
"""Ask dEQP for all test cases and removes non-Pass'ing ones.
This function will query dEQP for test cases and remove all cases that
are not in 'Pass'ing expectations from the list. This can be used
incrementally updating failing/hangin tests over several runs.
@param test_filter: string like '', 'dEQP-GLES3.stress'.
@return: List of dEQP tests to run.
test_cases = []
api = self._translate_name_to_api(test_filter)
if self._can_run(api):
executable = self._get_executable(api)
return test_cases
# Must be in the executable directory when running for it to find it's
# test data files!
not_passing_cases = self._load_not_passing_cases(test_filter)
# We did not find passing cases in expectations. Assume everything else
# that is there should not be run this time.
expectations_dir = os.path.join(self.bindir, 'expectations',
subset_spec = '%s.*' % test_filter
subset_paths = glob.glob(os.path.join(expectations_dir, subset_spec))
for subset_file in subset_paths:
# Filter against hasty failures only in hasty mode.
if self._hasty or '.hasty.' not in subset_file:
# Now ask dEQP executable nicely for whole list of tests. Needs to be
# run in executable directory. Output file is plain text file named
# e.g. 'dEQP-GLES2-cases.txt'.
command = ('%s '
'--deqp-runmode=txt-caselist '
'--deqp-surface-type=%s '
'--deqp-gl-config-name=rgba8888d24s8ms0 ' % (executable,
self._surface))'Running command %s', command),
# Now read this caselist file.
caselist_name = '%s-cases.txt' % test_filter.split('.')[0]
caselist_file = os.path.join(os.path.dirname(executable), caselist_name)
if not os.path.isfile(caselist_file):
raise error.TestFail('Failed: No caselist file at %s!' %
# And remove non-Pass'ing expectations from caselist.
caselist = open(caselist_file).read().splitlines()
# Contains lines like "TEST: dEQP-GLES2.capability"
test_cases = []
match = 'TEST: %s' % test_filter'Bootstrapping test cases matching "%s".', match)
for case in caselist:
if case.startswith(match):
case = case.split('TEST: ')[1]
test_cases = list(set(test_cases) - set(not_passing_cases))
if not test_cases:
raise error.TestFail('Failed: Unable to bootstrap %s!' % test_filter)
return test_cases
def _get_test_cases_from_names_file(self):
if os.path.exists(self._test_names_file):
file_path = self._test_names_file
test_cases = [line.rstrip('\n') for line in open(file_path)]
return [test for test in test_cases if test and not test.isspace()]
return []
def _get_test_cases(self, test_filter, subset):
"""Gets the test cases for 'Pass', 'Fail' etc. expectations.
This function supports bootstrapping of new GPU families and dEQP
binaries. In particular if there are not 'Pass' expectations found for
this GPU family it will query the dEQP executable for a list of all
available tests. It will then remove known non-'Pass'ing tests from
this list to avoid getting into hangs/crashes etc.
@param test_filter: string like '', 'dEQP-GLES3.stress'.
@param subset: string from 'Pass', 'Fail', 'Timeout' etc.
@return: List of dEQP tests to run.
expectations_dir = os.path.join(self.bindir, 'expectations',
subset_name = '%s.%s.bz2' % (test_filter, subset)
subset_path = os.path.join(expectations_dir, subset_name)
if not os.path.isfile(subset_path):
if subset == 'NotPass':
# TODO(ihf): Running hasty and NotPass together is an invitation
# for trouble (stability). Decide if it should be disallowed.
return self._load_not_passing_cases(test_filter)
if subset != 'Pass':
raise error.TestFail('Failed: No subset file found for %s!' %
# Ask dEQP for all cases and remove the failing ones.
return self._bootstrap_new_test_cases(test_filter)
test_cases = bz2.BZ2File(subset_path).read().splitlines()
if not test_cases:
raise error.TestFail(
'Failed: No test cases found in subset file %s!' % subset_path)
return test_cases
def _run_tests_individually(self, test_cases, failing_test=None):
"""Runs tests as isolated from each other, but slowly.
This function runs each test case separately as a command.
This means a new context for each test etc. Failures will be more
isolated, but runtime quite high due to overhead.
@param test_cases: List of dEQP test case strings.
@param failing_test: Tests considered failed will be appended to it.
@return: dictionary of test results.
test_results = {}
width = self._width
height = self._height
i = 0
for test_case in test_cases:
i += 1'[%d/%d] TestCase: %s', i, len(test_cases), test_case)
result_prefix = os.path.join(self._log_path, test_case)
log_file = '%s.log' % result_prefix
debug_file = '%s.debug' % result_prefix
api = self._translate_name_to_api(test_case)
if not self._can_run(api):
result = 'Skipped''Skipping on %s: %s', self._gpu_type, test_case)
executable = self._get_executable(api)
command = ('%s '
'--deqp-case=%s '
'--deqp-surface-type=%s '
'--deqp-gl-config-name=rgba8888d24s8ms0 '
'--deqp-log-images=disable '
'--deqp-watchdog=enable '
'--deqp-surface-width=%d '
'--deqp-surface-height=%d '
'--deqp-log-filename=%s' % (
logging.debug('Running single: %s', command)
# Must be in the executable directory when running for it to find it's
# test data files!
# Must initialize because some errors don't repopulate
# run_result, leaving old results.
run_result = {}
start_time = time.time()
run_result =,
result_counts = self._parse_test_results(
if result_counts:
result = result_counts.keys()[0]
result = 'Unknown'
except error.CmdTimeoutError:
result = 'TestTimeout'
except error.CmdError:
result = 'CommandFailed'
except Exception:
result = 'UnexpectedError'
end_time = time.time()
if self._debug:
# Collect debug info and save to json file.
output_msgs = {
'start_time': start_time,
'end_time': end_time,
'stdout': [],
'stderr': [],
'dmesg': []
logs = self._log_reader.get_logs()
output_msgs['dmesg'] = [
msg for msg in logs.splitlines()
if self._log_filter.match(msg)
if run_result:
output_msgs['stdout'] = run_result.stdout.splitlines()
output_msgs['stderr'] = run_result.stderr.splitlines()
with open(debug_file, 'w') as fd:
separators=(',', ' : '),
sort_keys=True)'Result: %s', result)
test_results[result] = test_results.get(result, 0) + 1
return test_results
def _run_tests_hasty(self, test_cases, failing_test=None):
"""Runs tests as quickly as possible.
This function runs all the test cases, but does not isolate tests and
may take shortcuts/not run all tests to provide maximum coverage at
minumum runtime.
@param test_cases: List of dEQP test case strings.
@param failing_test: Test considered failed will append to it.
@return: dictionary of test results.
# TODO(ihf): It saves half the test time to use 32*32 but a few tests
# fail as they need surfaces larger than 200*200.
width = self._width
height = self._height
results = {}
# All tests combined less than 1h in hasty.
batch_timeout = min(3600, self._timeout * self._hasty_batch_size)
num_test_cases = len(test_cases)
# We are dividing the number of tests into several shards but run them
# in smaller batches. We start and end at multiples of batch_size
# boundaries.
shard_start = self._hasty_batch_size * (
(self._shard_number * (num_test_cases / self._shard_count)) /
shard_end = self._hasty_batch_size * ((
(self._shard_number + 1) * (num_test_cases / self._shard_count)) /
# The last shard will be slightly larger than the others. Extend it to
# cover all test cases avoiding rounding problems with the integer
# arithmetics done to compute shard_start and shard_end.
if self._shard_number + 1 == self._shard_count:
shard_end = num_test_cases
for batch in xrange(shard_start, shard_end, self._hasty_batch_size):
batch_to = min(batch + self._hasty_batch_size, shard_end)
batch_cases = '\n'.join(test_cases[batch:batch_to])
# This assumes all tests in the batch are kicked off via the same
# executable.
api = self._translate_name_to_api(test_cases[batch])
if not self._can_run(api):'Skipping tests on %s: %s', self._gpu_type,
executable = self._get_executable(api)
log_file = os.path.join(self._log_path,
'%s_hasty_%d.log' % (self._filter, batch))
command = ('%s '
'--deqp-stdin-caselist '
'--deqp-surface-type=%s '
'--deqp-gl-config-name=rgba8888d24s8ms0 '
'--deqp-log-images=disable '
'--deqp-visibility=hidden '
'--deqp-watchdog=enable '
'--deqp-surface-width=%d '
'--deqp-surface-height=%d '
'--deqp-log-filename=%s' % (
)'Running tests %d...%d out of %d:\n%s\n%s',
batch + 1, batch_to, num_test_cases, command,
# Must be in the executable directory when running for it to
# find it's test data files!
except Exception:
# We are trying to handle all errors by parsing the log file.
results = self._parse_test_results(log_file, results,
return results
def _run_once(self, test_cases):
"""Run dEQP test_cases in individual/hasty mode.
@param test_cases: test cases to run.
failing_test = []
if self._hasty:'Running in hasty mode.')
test_results = self._run_tests_hasty(test_cases, failing_test)
else:'Running each test individually.')
test_results = self._run_tests_individually(test_cases,
return test_results, failing_test
def run_once(self, opts=None):
options = dict(filter='',
test_names='', # e.g.,,
# dEQP-GLES2.functional,
# dEQP-GLES3.accuracy.texture, etc.
subset_to_run='Pass', # Pass, Fail, Timeout, NotPass...
if opts is None:
opts = []
options.update(utils.args_to_dict(opts))'Test Options: %s', options)
self._hasty = (options['hasty'] == 'True')
self._timeout = int(options['timeout'])
self._test_names_file = options['test_names_file']
self._test_names = options['test_names']
self._shard_number = int(options['shard_number'])
self._shard_count = int(options['shard_count'])
self._debug = (options['debug'] == 'True')
if not (self._test_names_file or self._test_names):
self._filter = options['filter']
if not self._filter:
raise error.TestFail('Failed: No dEQP test filter specified')
if options['perf_failure_description']:
self._test_failure_description = options['perf_failure_description']
# Do not report failure if failure description is not specified.
self._test_failure_report_enable = False
# Some information to help post-process logs into blacklists later.'ChromeOS BOARD = %s', self._board)'ChromeOS CPU family = %s', self._cpu_type)'ChromeOS GPU family = %s', self._gpu_type)
# Create a place to put detailed test output logs.
filter_name = self._filter or os.path.basename(self._test_names_file)'dEQP test filter = %s', filter_name)
self._log_path = os.path.join(tempfile.gettempdir(), '%s-logs' %
shutil.rmtree(self._log_path, ignore_errors=True)
# Load either tests specified by test_names_file, test_names or filter.
test_cases = []
if self._test_names_file:
test_cases = self._get_test_cases_from_names_file()
elif self._test_names:
test_cases = []
for name in self._test_names.split(','):
test_cases.extend(self._get_test_cases(name, 'Pass'))
elif self._filter:
test_cases = self._get_test_cases(self._filter,
if self._debug:
# LogReader works on /var/log/messages by default.
self._log_reader = cros_logging.LogReader()
# Assume all tests failed at the beginning.
for test_case in test_cases:
test_results, failing_test = self._run_once(test_cases)
# Rerun the test if we are in hasty mode.
if self._hasty and len(failing_test) > 0:
if len(failing_test) < sum(test_results.values()) * RERUN_RATIO:"Because we are in hasty mode, we will rerun the "
"failing tests one at a time")
rerun_results, failing_test = self._run_once(failing_test)
# Update failing test result from the test_results
for result in test_results:
if result.lower() not in self.TEST_RESULT_FILTER:
test_results[result] = 0
for result in rerun_results:
test_results[result] = (test_results.get(result, 0) +
else:"There are too many failing tests. It would "
"take too long to rerun them. Giving up.")
# Update failing tests to the chrome perf dashboard records.
for test_case in test_cases:
if test_case not in failing_test:
self.remove_failures(test_case)'Test results:')
logging.debug('Test Failed: %s', failing_test)
test_count = 0
test_failures = 0
test_passes = 0
test_skipped = 0
for result in test_results:
test_count += test_results[result]
if result.lower() in ['pass']:
test_passes += test_results[result]
if result.lower() not in self.TEST_RESULT_FILTER:
test_failures += test_results[result]
if result.lower() in ['skipped']:
test_skipped += test_results[result]
# The text "Completed all tests." is used by the script
# and should always appear at the end of a completed test run.
'Completed all tests. Saw %d tests, %d passes and %d failures.',
test_count, test_passes, test_failures)
if self._filter and test_count == 0 and options[
'subset_to_run'] != 'NotPass':
logging.warning('No test cases found for filter: %s!', self._filter)
if test_failures:
raise error.TestFail('Failed: on %s %d/%d tests failed.' %
(self._gpu_type, test_failures, test_count))
if test_skipped > 0:'On %s %d tests skipped, %d passes' %
(self._gpu_type, test_skipped, test_passes))