1*9c5db199SXin Li#!/usr/bin/python3 -u 2*9c5db199SXin Li# 3*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 4*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 5*9c5db199SXin Li# found in the LICENSE file. 6*9c5db199SXin Li# 7*9c5db199SXin Li# Site extension of the default parser. Generate JSON reports and stack traces. 8*9c5db199SXin Li# 9*9c5db199SXin Li# This site parser is used to generate a JSON report of test failures, crashes, 10*9c5db199SXin Li# and the associated logs for later consumption by an Email generator. If any 11*9c5db199SXin Li# crashes are found, the debug symbols for the build are retrieved (either from 12*9c5db199SXin Li# Google Storage or local cache) and core dumps are symbolized. 13*9c5db199SXin Li# 14*9c5db199SXin Li# The parser uses the test report generator which comes bundled with the Chrome 15*9c5db199SXin Li# OS source tree in order to maintain consistency. As well as not having to keep 16*9c5db199SXin Li# track of any secondary failure allow lists. 17*9c5db199SXin Li# 18*9c5db199SXin Li# Stack trace generation is done by the minidump_stackwalk utility which is also 19*9c5db199SXin Li# bundled with the ChromeOS source tree. Requires gsutil and cros_sdk utilties 20*9c5db199SXin Li# be present in the path. 21*9c5db199SXin Li# 22*9c5db199SXin Li# The path to the ChromeOS source tree is defined in global_config under the 23*9c5db199SXin Li# CROS section as 'source_tree'. 24*9c5db199SXin Li# 25*9c5db199SXin Li# Existing parse behavior is kept completely intact. If the site parser is not 26*9c5db199SXin Li# configured it will print a debug message and exit after default parser is 27*9c5db199SXin Li# called. 28*9c5db199SXin Li# 29*9c5db199SXin Li 30*9c5db199SXin Lifrom __future__ import absolute_import 31*9c5db199SXin Lifrom __future__ import division 32*9c5db199SXin Lifrom __future__ import print_function 33*9c5db199SXin Li 34*9c5db199SXin Liimport errno 35*9c5db199SXin Liimport json 36*9c5db199SXin Liimport os 37*9c5db199SXin Liimport sys 38*9c5db199SXin Li 39*9c5db199SXin Liimport common 40*9c5db199SXin Lifrom autotest_lib.client.bin import utils 41*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config 42*9c5db199SXin Lifrom autotest_lib.tko import models 43*9c5db199SXin Lifrom autotest_lib.tko import parse 44*9c5db199SXin Lifrom autotest_lib.tko import utils as tko_utils 45*9c5db199SXin Lifrom autotest_lib.tko.parsers import version_0 46*9c5db199SXin Liimport six 47*9c5db199SXin Li 48*9c5db199SXin Li 49*9c5db199SXin Li# Name of the report file to produce upon completion. 50*9c5db199SXin Li_JSON_REPORT_FILE = 'results.json' 51*9c5db199SXin Li 52*9c5db199SXin Li# Number of log lines to include from error log with each test results. 53*9c5db199SXin Li_ERROR_LOG_LIMIT = 10 54*9c5db199SXin Li 55*9c5db199SXin Li# Status information is generally more useful than error log, so provide a lot. 56*9c5db199SXin Li_STATUS_LOG_LIMIT = 50 57*9c5db199SXin Li 58*9c5db199SXin Li 59*9c5db199SXin Liclass StackTrace(object): 60*9c5db199SXin Li """Handles all stack trace generation related duties. See generate().""" 61*9c5db199SXin Li 62*9c5db199SXin Li # Cache dir relative to chroot. 63*9c5db199SXin Li _CACHE_DIR = 'tmp/symbol-cache' 64*9c5db199SXin Li 65*9c5db199SXin Li # Flag file indicating symbols have completed processing. One is created in 66*9c5db199SXin Li # each new symbols directory. 67*9c5db199SXin Li _COMPLETE_FILE = '.completed' 68*9c5db199SXin Li 69*9c5db199SXin Li # Maximum cache age in days; all older cache entries will be deleted. 70*9c5db199SXin Li _MAX_CACHE_AGE_DAYS = 1 71*9c5db199SXin Li 72*9c5db199SXin Li # Directory inside of tarball under which the actual symbols are stored. 73*9c5db199SXin Li _SYMBOL_DIR = 'debug/breakpad' 74*9c5db199SXin Li 75*9c5db199SXin Li # Maximum time to wait for another instance to finish processing symbols. 76*9c5db199SXin Li _SYMBOL_WAIT_TIMEOUT = 10 * 60 77*9c5db199SXin Li 78*9c5db199SXin Li 79*9c5db199SXin Li def __init__(self, results_dir, cros_src_dir): 80*9c5db199SXin Li """Initializes class variables. 81*9c5db199SXin Li 82*9c5db199SXin Li Args: 83*9c5db199SXin Li results_dir: Full path to the results directory to process. 84*9c5db199SXin Li cros_src_dir: Full path to ChromeOS source tree. Must have a 85*9c5db199SXin Li working chroot. 86*9c5db199SXin Li """ 87*9c5db199SXin Li self._results_dir = results_dir 88*9c5db199SXin Li self._cros_src_dir = cros_src_dir 89*9c5db199SXin Li self._chroot_dir = os.path.join(self._cros_src_dir, 'chroot') 90*9c5db199SXin Li 91*9c5db199SXin Li 92*9c5db199SXin Li def _get_cache_dir(self): 93*9c5db199SXin Li """Returns a path to the local cache dir, creating if nonexistent. 94*9c5db199SXin Li 95*9c5db199SXin Li Symbol cache is kept inside the chroot so we don't have to mount it into 96*9c5db199SXin Li chroot for symbol generation each time. 97*9c5db199SXin Li 98*9c5db199SXin Li Returns: 99*9c5db199SXin Li A path to the local cache dir. 100*9c5db199SXin Li """ 101*9c5db199SXin Li cache_dir = os.path.join(self._chroot_dir, self._CACHE_DIR) 102*9c5db199SXin Li if not os.path.exists(cache_dir): 103*9c5db199SXin Li try: 104*9c5db199SXin Li os.makedirs(cache_dir) 105*9c5db199SXin Li except OSError as e: 106*9c5db199SXin Li if e.errno != errno.EEXIST: 107*9c5db199SXin Li raise 108*9c5db199SXin Li return cache_dir 109*9c5db199SXin Li 110*9c5db199SXin Li 111*9c5db199SXin Li def _get_job_name(self): 112*9c5db199SXin Li """Returns job name read from 'label' keyval in the results dir. 113*9c5db199SXin Li 114*9c5db199SXin Li Returns: 115*9c5db199SXin Li Job name string. 116*9c5db199SXin Li """ 117*9c5db199SXin Li return models.job.read_keyval(self._results_dir).get('label') 118*9c5db199SXin Li 119*9c5db199SXin Li 120*9c5db199SXin Li def _parse_job_name(self, job_name): 121*9c5db199SXin Li """Returns a tuple of (board, rev, version) parsed from the job name. 122*9c5db199SXin Li 123*9c5db199SXin Li Handles job names of the form "<board-rev>-<version>...", 124*9c5db199SXin Li "<board-rev>-<rev>-<version>...", and 125*9c5db199SXin Li "<board-rev>-<rev>-<version_0>_to_<version>..." 126*9c5db199SXin Li 127*9c5db199SXin Li Args: 128*9c5db199SXin Li job_name: A job name of the format detailed above. 129*9c5db199SXin Li 130*9c5db199SXin Li Returns: 131*9c5db199SXin Li A tuple of (board, rev, version) parsed from the job name. 132*9c5db199SXin Li """ 133*9c5db199SXin Li version = job_name.rsplit('-', 3)[1].split('_')[-1] 134*9c5db199SXin Li arch, board, rev = job_name.split('-', 3)[:3] 135*9c5db199SXin Li return '-'.join([arch, board]), rev, version 136*9c5db199SXin Li 137*9c5db199SXin Li 138*9c5db199SXin Lidef parse_reason(path): 139*9c5db199SXin Li """Process status.log or status and return a test-name: reason dict.""" 140*9c5db199SXin Li status_log = os.path.join(path, 'status.log') 141*9c5db199SXin Li if not os.path.exists(status_log): 142*9c5db199SXin Li status_log = os.path.join(path, 'status') 143*9c5db199SXin Li if not os.path.exists(status_log): 144*9c5db199SXin Li return 145*9c5db199SXin Li 146*9c5db199SXin Li reasons = {} 147*9c5db199SXin Li last_test = None 148*9c5db199SXin Li for line in open(status_log).readlines(): 149*9c5db199SXin Li try: 150*9c5db199SXin Li # Since we just want the status line parser, it's okay to use the 151*9c5db199SXin Li # version_0 parser directly; all other parsers extend it. 152*9c5db199SXin Li status = version_0.status_line.parse_line(line) 153*9c5db199SXin Li except: 154*9c5db199SXin Li status = None 155*9c5db199SXin Li 156*9c5db199SXin Li # Assemble multi-line reasons into a single reason. 157*9c5db199SXin Li if not status and last_test: 158*9c5db199SXin Li reasons[last_test] += line 159*9c5db199SXin Li 160*9c5db199SXin Li # Skip non-lines, empty lines, and successful tests. 161*9c5db199SXin Li if not status or not status.reason.strip() or status.status == 'GOOD': 162*9c5db199SXin Li continue 163*9c5db199SXin Li 164*9c5db199SXin Li # Update last_test name, so we know which reason to append multi-line 165*9c5db199SXin Li # reasons to. 166*9c5db199SXin Li last_test = status.testname 167*9c5db199SXin Li reasons[last_test] = status.reason 168*9c5db199SXin Li 169*9c5db199SXin Li return reasons 170*9c5db199SXin Li 171*9c5db199SXin Li 172*9c5db199SXin Lidef main(): 173*9c5db199SXin Li # Call the original parser. 174*9c5db199SXin Li parse.main() 175*9c5db199SXin Li 176*9c5db199SXin Li # Results directory should be the last argument passed in. 177*9c5db199SXin Li results_dir = sys.argv[-1] 178*9c5db199SXin Li 179*9c5db199SXin Li # Load the ChromeOS source tree location. 180*9c5db199SXin Li cros_src_dir = global_config.global_config.get_config_value( 181*9c5db199SXin Li 'CROS', 'source_tree', default='') 182*9c5db199SXin Li 183*9c5db199SXin Li # We want the standard Autotest parser to keep working even if we haven't 184*9c5db199SXin Li # been setup properly. 185*9c5db199SXin Li if not cros_src_dir: 186*9c5db199SXin Li tko_utils.dprint( 187*9c5db199SXin Li 'Unable to load required components for site parser. Falling back' 188*9c5db199SXin Li ' to default parser.') 189*9c5db199SXin Li return 190*9c5db199SXin Li 191*9c5db199SXin Li # Load ResultCollector from the ChromeOS source tree. 192*9c5db199SXin Li sys.path.append(os.path.join( 193*9c5db199SXin Li cros_src_dir, 'src/platform/crostestutils/utils_py')) 194*9c5db199SXin Li from generate_test_report import ResultCollector 195*9c5db199SXin Li 196*9c5db199SXin Li # Collect results using the standard ChromeOS test report generator. Doing 197*9c5db199SXin Li # so allows us to use the same crash allow list and reporting standards the 198*9c5db199SXin Li # VM based test instances use. 199*9c5db199SXin Li # TODO(scottz): Reevaluate this code usage. crosbug.com/35282 200*9c5db199SXin Li results = ResultCollector().RecursivelyCollectResults(results_dir) 201*9c5db199SXin Li # We don't care about successful tests. We only want failed or crashing. 202*9c5db199SXin Li # Note: list([]) generates a copy of the dictionary, so it's safe to delete. 203*9c5db199SXin Li for test_status in list(results): 204*9c5db199SXin Li if test_status['crashes']: 205*9c5db199SXin Li continue 206*9c5db199SXin Li elif test_status['status'] == 'PASS': 207*9c5db199SXin Li results.remove(test_status) 208*9c5db199SXin Li 209*9c5db199SXin Li # Filter results and collect logs. If we can't find a log for the test, skip 210*9c5db199SXin Li # it. The Emailer will fill in the blanks using Database data later. 211*9c5db199SXin Li filtered_results = {} 212*9c5db199SXin Li for test_dict in results: 213*9c5db199SXin Li result_log = '' 214*9c5db199SXin Li test_name = os.path.basename(test_dict['testdir']) 215*9c5db199SXin Li error = os.path.join( 216*9c5db199SXin Li test_dict['testdir'], 'debug', '%s.ERROR' % test_name) 217*9c5db199SXin Li 218*9c5db199SXin Li # If the error log doesn't exist, we don't care about this test. 219*9c5db199SXin Li if not os.path.isfile(error): 220*9c5db199SXin Li continue 221*9c5db199SXin Li 222*9c5db199SXin Li # Parse failure reason for this test. 223*9c5db199SXin Li for t, r in six.iteritems(parse_reason(test_dict['testdir'])): 224*9c5db199SXin Li # Server tests may have subtests which will each have their own 225*9c5db199SXin Li # reason, so display the test name for the subtest in that case. 226*9c5db199SXin Li if t != test_name: 227*9c5db199SXin Li result_log += '%s: ' % t 228*9c5db199SXin Li result_log += '%s\n\n' % r.strip() 229*9c5db199SXin Li 230*9c5db199SXin Li # Trim results_log to last _STATUS_LOG_LIMIT lines. 231*9c5db199SXin Li short_result_log = '\n'.join( 232*9c5db199SXin Li result_log.splitlines()[-1 * _STATUS_LOG_LIMIT:]).strip() 233*9c5db199SXin Li 234*9c5db199SXin Li # Let the reader know we've trimmed the log. 235*9c5db199SXin Li if short_result_log != result_log.strip(): 236*9c5db199SXin Li short_result_log = ( 237*9c5db199SXin Li '[...displaying only the last %d status log lines...]\n%s' % ( 238*9c5db199SXin Li _STATUS_LOG_LIMIT, short_result_log)) 239*9c5db199SXin Li 240*9c5db199SXin Li # Pull out only the last _LOG_LIMIT lines of the file. 241*9c5db199SXin Li short_log = utils.system_output('tail -n %d %s' % ( 242*9c5db199SXin Li _ERROR_LOG_LIMIT, error)) 243*9c5db199SXin Li 244*9c5db199SXin Li # Let the reader know we've trimmed the log. 245*9c5db199SXin Li if len(short_log.splitlines()) == _ERROR_LOG_LIMIT: 246*9c5db199SXin Li short_log = ( 247*9c5db199SXin Li '[...displaying only the last %d error log lines...]\n%s' % ( 248*9c5db199SXin Li _ERROR_LOG_LIMIT, short_log)) 249*9c5db199SXin Li 250*9c5db199SXin Li filtered_results[test_name] = test_dict 251*9c5db199SXin Li filtered_results[test_name]['log'] = '%s\n\n%s' % ( 252*9c5db199SXin Li short_result_log, short_log) 253*9c5db199SXin Li 254*9c5db199SXin Li # Generate JSON dump of results. Store in results dir. 255*9c5db199SXin Li json_file = open(os.path.join(results_dir, _JSON_REPORT_FILE), 'w') 256*9c5db199SXin Li json.dump(filtered_results, json_file) 257*9c5db199SXin Li json_file.close() 258*9c5db199SXin Li 259*9c5db199SXin Li 260*9c5db199SXin Liif __name__ == '__main__': 261*9c5db199SXin Li main() 262