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