graphics_dEQP.py revision 3152d7b9e9fc8f6b724bb426b8af68ba81a0077f
1# Copyright 2014 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import bz2 6import glob 7import logging 8import os 9import shutil 10import tempfile 11import xml.etree.cElementTree as et 12from autotest_lib.client.bin import test, utils 13from autotest_lib.client.common_lib import error 14from autotest_lib.client.cros import service_stopper 15 16 17class graphics_dEQP(test.test): 18 """Run the drawElements Quality Program test suite. 19 """ 20 version = 1 21 _services = None 22 _hasty = False 23 _hasty_batch_size = 100 # Batch size in hasty mode. 24 _board = None 25 _cpu_type = None 26 _gpu_type = None 27 _filter = None 28 _width = 256 # Use smallest width for which all tests run/pass. 29 _height = 256 # Use smallest height for which all tests run/pass. 30 _timeout = 70 # Larger than twice the dEQP watchdog timeout at 30s. 31 _test_names = None 32 33 DEQP_BASEDIR = '/usr/local/deqp' 34 DEQP_MODULES = { 35 'dEQP-EGL': 'egl', 36 'dEQP-GLES2': 'gles2', 37 'dEQP-GLES3': 'gles3', 38 'dEQP-GLES31': 'gles31' 39 } 40 41 def initialize(self): 42 self._gpu_type = utils.get_gpu_family() 43 self._cpu_type = utils.get_cpu_soc_family() 44 self._board = utils.get_board() 45 self._services = service_stopper.ServiceStopper(['ui', 'powerd']) 46 47 def cleanup(self): 48 if self._services: 49 self._services.restore_services() 50 51 def _parse_test_results(self, result_filename, test_results=None): 52 """Handles result files with one or more test results. 53 54 @param result_filename: log file to parse. 55 56 @return: dictionary of parsed test results. 57 """ 58 xml = '' 59 xml_start = False 60 xml_complete = False 61 xml_bad = False 62 result = 'ParseTestResultFail' 63 64 if test_results is None: 65 test_results = {} 66 67 if not os.path.isfile(result_filename): 68 return test_results 69 # TODO(ihf): Add names of failing tests to a list in the results. 70 with open(result_filename) as result_file: 71 for line in result_file.readlines(): 72 # If the test terminates early, the XML will be incomplete 73 # and should not be parsed. 74 if line.startswith('#terminateTestCaseResult'): 75 result = line.strip().split()[1] 76 xml_bad = True 77 # Will only see #endTestCaseResult if the test does not 78 # terminate early. 79 elif line.startswith('#endTestCaseResult'): 80 xml_complete = True 81 elif xml_start: 82 xml += line 83 elif line.startswith('#beginTestCaseResult'): 84 # If we see another begin before an end then something is 85 # wrong. 86 if xml_start: 87 xml_bad = True 88 else: 89 xml_start = True 90 91 if xml_complete or xml_bad: 92 if xml_complete: 93 root = et.fromstring(xml) 94 result = root.find('Result').get('StatusCode').strip() 95 xml_complete = False 96 test_results[result] = test_results.get(result, 0) + 1 97 xml_bad = False 98 xml_start = False 99 result = 'ParseTestResultFail' 100 xml = '' 101 102 return test_results 103 104 def _bootstrap_new_test_cases(self, executable, test_filter): 105 """Ask dEQP for all test cases and removes non-Pass'ing ones. 106 107 This function assumes that the '*.Pass' file does not exist and that 108 everything else found in the directory should be removed from the tests 109 to run. This can be used incrementally updating failing/hangin tests 110 over several runs. 111 112 @param executable: location '/usr/local/deqp/modules/gles2/deqp-gles2'. 113 @param test_filter: string like 'dEQP-GLES2.info', 'dEQP-GLES3.stress'. 114 115 @return: List of dEQP tests to run. 116 """ 117 test_cases = [] 118 not_passing_cases = [] 119 # We did not find passing cases in expectations. Assume everything else 120 # that is there should not be run this time. 121 expectations_dir = os.path.join(self.bindir, 'expectations', 122 self._gpu_type) 123 subset_spec = '%s.*' % test_filter 124 subset_paths = glob.glob(os.path.join(expectations_dir, subset_spec)) 125 for subset_file in subset_paths: 126 # Filter against hasty failures only in hasty mode. 127 if self._hasty or '.hasty.bz2' not in subset_file: 128 not_passing_cases.extend( 129 bz2.BZ2File(subset_file).read().splitlines()) 130 131 # Now ask dEQP executable nicely for whole list of tests. Needs to be 132 # run in executable directory. Output file is plain text file named 133 # e.g. 'dEQP-GLES2-cases.txt'. 134 command = ('%s ' 135 '--deqp-runmode=txt-caselist ' 136 '--deqp-surface-type=fbo ' % executable) 137 logging.info('Running command %s', command) 138 utils.run(command, 139 timeout=60, 140 stderr_is_expected=False, 141 ignore_status=False, 142 stdin=None, 143 stdout_tee=utils.TEE_TO_LOGS, 144 stderr_tee=utils.TEE_TO_LOGS) 145 146 # Now read this caselist file. 147 caselist_name = '%s-cases.txt' % test_filter.split('.')[0] 148 caselist_file = os.path.join(os.path.dirname(executable), caselist_name) 149 if not os.path.isfile(caselist_file): 150 raise error.TestError('No caselist file at %s!' % caselist_file) 151 152 # And remove non-Pass'ing expectations from caselist. 153 caselist = open(caselist_file).read().splitlines() 154 # Contains lines like "TEST: dEQP-GLES2.capability" 155 test_cases = [] 156 match = 'TEST: %s' % test_filter 157 logging.info('Bootstrapping test cases matching "%s".', match) 158 for case in caselist: 159 if case.startswith(match): 160 case = case.split('TEST: ')[1] 161 test_cases.append(case) 162 163 test_cases = list(set(test_cases) - set(not_passing_cases)) 164 if not test_cases: 165 raise error.TestError('Unable to bootstrap %s!' % test_filter) 166 # TODO(ihf): Sorting is nice but can introduce errors due to changed 167 # ordering and state leakage. Consider deleting this if it causes 168 # too much problems. 169 test_cases.sort() 170 return test_cases 171 172 def _get_test_cases(self, executable, test_filter, subset): 173 """Gets the test cases for 'Pass', 'Fail' etc. expectations. 174 175 This function supports bootstrapping of new GPU families and dEQP 176 binaries. In particular if there are not 'Pass' expectations found for 177 this GPU family it will query the dEQP executable for a list of all 178 available tests. It will then remove known non-'Pass'ing tests from this 179 list to avoid getting into hangs/crashes etc. 180 181 @param executable: location '/usr/local/deqp/modules/gles2/deqp-gles2'. 182 @param test_filter: string like 'dEQP-GLES2.info', 'dEQP-GLES3.stress'. 183 @param subset: string from 'Pass', 'Fail', 'Timeout' etc. 184 185 @return: List of dEQP tests to run. 186 """ 187 expectations_dir = os.path.join(self.bindir, 'expectations', 188 self._gpu_type) 189 subset_name = '%s.%s.bz2' % (test_filter, subset) 190 subset_path = os.path.join(expectations_dir, subset_name) 191 if not os.path.isfile(subset_path): 192 if subset != 'Pass': 193 raise error.TestError('No subset file found for %s!' % 194 subset_path) 195 return self._bootstrap_new_test_cases(executable, test_filter) 196 197 test_cases = bz2.BZ2File(subset_path).read().splitlines() 198 if not test_cases: 199 raise error.TestError('No test cases found in subset file %s!' % 200 subset_path) 201 return test_cases 202 203 def run_tests_individually(self, executable, test_cases): 204 """Runs tests as isolated from each other, but slowly. 205 206 This function runs each test case separately as a command. 207 This means a new context for each test etc. Failures will be more 208 isolated, but runtime quite high due to overhead. 209 210 @param executable: dEQP executable path. 211 @param test_cases: List of dEQP test case strings. 212 213 @return: dictionary of test results. 214 """ 215 test_results = {} 216 width = self._width 217 height = self._height 218 219 log_path = os.path.join(tempfile.gettempdir(), '%s-logs' % self._filter) 220 shutil.rmtree(log_path, ignore_errors=True) 221 os.mkdir(log_path) 222 223 i = 0 224 for test_case in test_cases: 225 i += 1 226 logging.info('[%d/%d] TestCase: %s', i, len(test_cases), test_case) 227 log_file = '%s.log' % os.path.join(log_path, test_case) 228 command = ('%s ' 229 '--deqp-case=%s ' 230 '--deqp-surface-type=fbo ' 231 '--deqp-log-images=disable ' 232 '--deqp-watchdog=enable ' 233 '--deqp-surface-width=%d ' 234 '--deqp-surface-height=%d ' 235 '--deqp-log-filename=%s' % 236 (executable, test_case, width, height, log_file)) 237 238 try: 239 logging.info('Running single: %s', command) 240 utils.run(command, 241 timeout=self._timeout, 242 stderr_is_expected=False, 243 ignore_status=True, 244 stdout_tee=utils.TEE_TO_LOGS, 245 stderr_tee=utils.TEE_TO_LOGS) 246 result_counts = self._parse_test_results(log_file) 247 if result_counts: 248 result = result_counts.keys()[0] 249 else: 250 result = 'Unknown' 251 except error.CmdTimeoutError: 252 result = 'TestTimeout' 253 except error.CmdError: 254 result = 'CommandFailed' 255 except Exception: 256 result = 'UnexpectedError' 257 258 logging.info('Result: %s', result) 259 test_results[result] = test_results.get(result, 0) + 1 260 261 return test_results 262 263 def run_tests_hasty(self, executable, test_cases): 264 """Runs tests as quickly as possible. 265 266 This function runs all the test cases, but does not isolate tests and 267 may take shortcuts/not run all tests to provide maximum coverage at 268 minumum runtime. 269 270 @param executable: dEQP executable path. 271 @param test_cases: List of dEQP test case strings. 272 273 @return: dictionary of test results. 274 """ 275 # TODO(ihf): It saves half the test time to use 32*32 but a few tests 276 # fail as they need surfaces larger than 200*200. 277 width = self._width 278 height = self._height 279 results = {} 280 # All tests combined less than 1h in hasty. 281 batch_timeout = min(3600, self._timeout * self._hasty_batch_size) 282 num_test_cases = len(test_cases) 283 # We are trying to handle all errors by parsing the log file. 284 for batch in xrange(0, num_test_cases, self._hasty_batch_size): 285 batch_to = min(batch + self._hasty_batch_size, num_test_cases) 286 batch_cases = '\n'.join(test_cases[batch:batch_to]) 287 command = ('%s ' 288 '--deqp-stdin-caselist ' 289 '--deqp-surface-type=fbo ' 290 '--deqp-log-images=disable ' 291 '--deqp-visibility=hidden ' 292 '--deqp-watchdog=enable ' 293 '--deqp-surface-width=%d ' 294 '--deqp-surface-height=%d ' % (executable, width, 295 height)) 296 297 log_file = os.path.join(tempfile.gettempdir(), 298 '%s_hasty_%d.log' % (self._filter, batch)) 299 if os.path.exists(log_file): 300 os.remove(log_file) 301 command += '--deqp-log-filename=' + log_file 302 logging.info('Running tests %d...%d out of %d:\n%s\n%s', batch + 1, 303 batch_to, num_test_cases, command, batch_cases) 304 305 try: 306 utils.run(command, 307 timeout=batch_timeout, 308 stderr_is_expected=False, 309 ignore_status=False, 310 stdin=batch_cases, 311 stdout_tee=utils.TEE_TO_LOGS, 312 stderr_tee=utils.TEE_TO_LOGS) 313 except Exception: 314 pass 315 results = self._parse_test_results(log_file, results) 316 logging.info(results) 317 return results 318 319 def run_once(self, opts=None): 320 options = dict(filter='', 321 test_names='', # e.g., dEQP-GLES3.info.version, 322 # dEQP-GLES2.functional, 323 # dEQP-GLES3.accuracy.texture, etc. 324 timeout=self._timeout, 325 subset_to_run='Pass', # Pass, Fail, Timeout etc. 326 hasty='False', 327 shard=1, # TODO(ihf): Support sharding for bvt-cq. 328 shards=1) 329 if opts is None: 330 opts = [] 331 options.update(utils.args_to_dict(opts)) 332 logging.info('Test Options: %s', options) 333 334 self._hasty = (options['hasty'] == 'True') 335 self._timeout = int(options['timeout']) 336 self._test_names = options['test_names'] 337 if not self._test_names: 338 self._filter = options['filter'] 339 if not self._filter: 340 raise error.TestError('No dEQP test filter specified') 341 342 # Some information to help postprocess logs into blacklists later. 343 logging.info('ChromeOS BOARD = %s', self._board) 344 logging.info('ChromeOS CPU family = %s', self._cpu_type) 345 logging.info('ChromeOS GPU family = %s', self._gpu_type) 346 logging.info('dEQP test filter = %s', self._filter) 347 348 # Determine module from test_names or filter. 349 if self._test_names: 350 test_prefix = self._test_names.split('.')[0] 351 self._filter = '%s.filter_args' % test_prefix 352 elif self._filter: 353 test_prefix = self._filter.split('.')[0] 354 if test_prefix in self.DEQP_MODULES: 355 module = self.DEQP_MODULES[test_prefix] 356 elif self._test_names: 357 raise error.TestError('Invalid test names: %s' % self._test_names) 358 else: 359 raise error.TestError('Invalid test filter: %s' % self._filter) 360 361 executable_path = os.path.join(self.DEQP_BASEDIR, 'modules', module) 362 executable = os.path.join(executable_path, 'deqp-%s' % module) 363 364 self._services.stop_services() 365 366 # Must be in the executable directory when running for it to find it's 367 # test data files! 368 os.chdir(executable_path) 369 if self._test_names: 370 test_cases = [] 371 for name in self._test_names.split(','): 372 test_cases.extend(self._get_test_cases(executable, name, 373 'Pass')) 374 else: 375 test_cases = self._get_test_cases(executable, self._filter, 376 options['subset_to_run']) 377 378 test_results = {} 379 if self._hasty: 380 logging.info('Running in hasty mode.') 381 test_results = self.run_tests_hasty(executable, test_cases) 382 else: 383 logging.info('Running each test individually.') 384 test_results = self.run_tests_individually(executable, test_cases) 385 386 logging.info('Test results:') 387 logging.info(test_results) 388 self.write_perf_keyval(test_results) 389 390 test_count = 0 391 test_failures = 0 392 for result in test_results: 393 test_count += test_results[result] 394 if result.lower() not in ['pass', 'notsupported', 'internalerror']: 395 test_failures += test_results[result] 396 # The text "Completed all tests." is used by the process_log.py script 397 # and should always appear at the end of a completed test run. 398 logging.info('Completed all tests. Saw %d tests and %d failures.', 399 test_count, test_failures) 400 401 if test_failures: 402 raise error.TestFail('%d/%d tests failed.' % 403 (test_failures, test_count)) 404 405 if test_count == 0: 406 raise error.TestError('No test cases found for filter: %s!' % 407 self._filter) 408