• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2019 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
5'''Utilities to summarize TKO results reported by tests in the suite.'''
6
7from __future__ import absolute_import
8from __future__ import division
9from __future__ import print_function
10
11import argparse
12import collections
13import contextlib
14import mysql.connector
15
16def Error(Exception):
17    """Error detected in this script."""
18
19
20# Row corresponds to a single row of tko_test_view_2 table in AFE DB, but
21# contains only a subset of the columns in the table.
22Row = collections.namedtuple(
23        'Row',
24        'name, status, reason'
25)
26
27
28def get(conn, task_ids):
29    """Get tko_test_view_2 Row()s for given skylab task_ids.
30
31    @param conn: A MySQL connection to TKO.
32    @param task_ids: list of Skylab task request IDs to collect test views for.
33    @return: {task_id: [Row(...)...]}
34    """
35    try:
36        task_job_ids = _get_job_idxs_from_tko(conn, task_ids)
37        job_task_ids = {v: k for k, v in task_job_ids.iteritems()}
38        job_rows = _get_rows_from_tko(conn, job_task_ids.keys())
39        return {job_task_ids[k]: v for k, v in job_rows.iteritems()}
40    finally:
41        conn.close()
42
43
44def filter_failed(rows):
45    """Filter down given list of test_views Row() to failed tests."""
46    return [r for r in rows if r.status in _BAD_STATUSES]
47
48
49def main():
50    '''Entry-point to use this script standalone.'''
51    parser = argparse.ArgumentParser(
52            description='Summarize TKO results for a Skylab task')
53    parser.add_argument(
54            '--task-id',
55            action='append',
56            help='Swarming request ID for the skylab task (may be repeated)',
57    )
58    parser.add_argument(
59            '--host',
60            required=True,
61            help='TKO host IP',
62    )
63    parser.add_argument(
64            '--port',
65            type=int,
66            default=3306,
67            help='TKO port',
68    )
69    parser.add_argument(
70            '--user',
71            required=True,
72            help='TKO MySQL user',
73    )
74    parser.add_argument(
75            '--password',
76            required=True,
77            help='TKO MySQL password',
78    )
79    args = parser.parse_args()
80    if not args.task_id:
81        raise Error('Must request at least one --task-id')
82
83    conn = mysql.connector.connect(
84            host=args.host,
85            port=args.port,
86            user=args.user,
87            password=args.password,
88            database='chromeos_autotest_db',
89    )
90    views = get(conn, args.task_id)
91    for task_id, rows in views.iteritems():
92        print('Task ID: %s' % task_id)
93        for row in filter_failed(rows):
94            print('  %s in status %s' % (row.name, row.status))
95            print('    reason: %s' % (row.reason,))
96        print('')
97
98
99_BAD_STATUSES = {
100        'ABORT',
101        'ERROR',
102        'FAIL',
103}
104
105
106def _get_rows_from_tko(conn, tko_job_ids):
107    """Get a list of Row() for the given TKO job IDs.
108
109    @param conn: A MySQL connection.
110    @param job_ids: List of tko_job_ids to get Row()s for.
111    @return: {tko_job_id: [Row]}
112    """
113    job_rows = collections.defaultdict(list)
114    statuses = _get_status_map(conn)
115
116    _GET_TKO_TEST_VIEW_2 = """
117    SELECT job_idx, test_name, status_idx, reason FROM tko_test_view_2
118            WHERE invalid = 0 AND job_idx IN (%s)
119    """
120    q = _GET_TKO_TEST_VIEW_2 % ', '.join(['%s'] * len(tko_job_ids))
121    with _cursor(conn) as cursor:
122        cursor.execute(q, tko_job_ids)
123        for job_idx, name, s_idx, reason in cursor.fetchall():
124            job_rows[job_idx].append(
125                    Row(name, statuses.get(s_idx, 'UNKNOWN'), reason))
126    return dict(job_rows)
127
128
129def _get_job_idxs_from_tko(conn, task_ids):
130    """Get tko_job_idx for given task_ids.
131
132    Task execution reports the run ID to TKO, but Skylab clients only knows the
133    request ID of the created task.
134    Swarming executes a task with increasing run IDs, retrying on bot failure.
135    If a task is retried after the point where TKO results are reported, this
136    function returns the TKO job_idx corresponding to the last completed
137    attempt.
138
139    @param conn: MySQL connection to TKO.
140    @param task_ids: List of task request IDs to get TKO job IDs for.
141    @return {task_id: job_id}
142    """
143    task_runs = {}
144    run_ids = []
145    for task_id in task_ids:
146        run_ids += _run_ids_for_request(task_id)
147        task_runs[task_id] = list(reversed(run_ids))
148    run_job_idxs = _get_job_idxs_for_run_ids(conn, run_ids)
149
150    task_job_idxs = {}
151    for task_id, run_ids in task_runs.iteritems():
152        for run_id in run_ids:
153            if run_id in run_job_idxs:
154                task_job_idxs[task_id] = run_job_idxs[run_id]
155                break
156    return task_job_idxs
157
158
159def _get_job_idxs_for_run_ids(conn, run_ids):
160    """Get tko_job_idx for a given task run_ids.
161
162    @param conn: MySQL connection to TKO.
163    @param task_ids: List of task run IDs to get TKO job IDs for.
164    @return {run_id: job_id}
165    """
166    _GET_TKO_JOB_Q = """
167    SELECT task_id, tko_job_idx FROM tko_task_references
168            WHERE reference_type = "skylab" AND task_id IN (%s)
169    """
170    q = _GET_TKO_JOB_Q % ', '.join(['%s'] * len(run_ids))
171
172    job_idxs = {}
173    with _cursor(conn) as cursor:
174        cursor.execute(q, run_ids)
175        for run_id, tko_job_idx in cursor.fetchall():
176            if run_id in job_idxs:
177                raise Error('task run ID %s has multiple tko references' %
178                            (run_id,))
179            job_idxs[run_id] = tko_job_idx
180    return job_idxs
181
182
183def _get_status_map(conn):
184    statuses = {}
185    with _cursor(conn) as cursor:
186        cursor.execute('SELECT status_idx, word FROM tko_status')
187        r = cursor.fetchall()
188        for idx, word in r:
189            statuses[idx] = word
190    return statuses
191
192
193def _run_ids_for_request(request_id):
194    """Return Swarming run IDs for a given request ID, in ascending order."""
195    prefix = request_id[:len(request_id)-1]
196    return [prefix + i for i in ('1', '2')]
197
198
199@contextlib.contextmanager
200def _cursor(conn):
201    c = conn.cursor()
202    try:
203        yield c
204    finally:
205        c.close()
206
207
208if __name__ == '__main__':
209  main()
210