xref: /aosp_15_r20/external/autotest/tko/site_parse.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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