xref: /aosp_15_r20/external/autotest/server/lib/suite_report.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Copyright 2016 The Chromium OS Authors. All rights reserved.
2*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
3*9c5db199SXin Li# found in the LICENSE file.
4*9c5db199SXin Li
5*9c5db199SXin Li"""Services relating to generating a suite timeline and report."""
6*9c5db199SXin Li
7*9c5db199SXin Lifrom __future__ import print_function
8*9c5db199SXin Li
9*9c5db199SXin Liimport common
10*9c5db199SXin Liimport datetime
11*9c5db199SXin Liimport json
12*9c5db199SXin Liimport logging
13*9c5db199SXin Li
14*9c5db199SXin Lifrom autotest_lib.client.common_lib import time_utils
15*9c5db199SXin Lifrom autotest_lib.server import frontend
16*9c5db199SXin Lifrom autotest_lib.server.lib import status_history
17*9c5db199SXin Li
18*9c5db199SXin Li
19*9c5db199SXin LiHostJobHistory = status_history.HostJobHistory
20*9c5db199SXin Li
21*9c5db199SXin Li# TODO: Handle other statuses like infra failures.
22*9c5db199SXin LiTKO_STATUS_MAP = {
23*9c5db199SXin Li    'ERROR': 'fail',
24*9c5db199SXin Li    'FAIL': 'fail',
25*9c5db199SXin Li    'GOOD': 'pass',
26*9c5db199SXin Li    'PASS': 'pass',
27*9c5db199SXin Li    'ABORT': 'aborted',
28*9c5db199SXin Li    'Failed': 'fail',
29*9c5db199SXin Li    'Completed': 'pass',
30*9c5db199SXin Li    'Aborted': 'aborted',
31*9c5db199SXin Li}
32*9c5db199SXin Li
33*9c5db199SXin Li
34*9c5db199SXin Li# Default suite timeout in seconds
35*9c5db199SXin LiDEFAULT_SUITE_TIMEOUT = 90 * 60
36*9c5db199SXin Li
37*9c5db199SXin Li
38*9c5db199SXin Lidef to_epoch_time_int(value):
39*9c5db199SXin Li    """Convert the given value to epoch time int.
40*9c5db199SXin Li
41*9c5db199SXin Li    @returns: epoch time in integer."""
42*9c5db199SXin Li    return int(time_utils.to_epoch_time(value))
43*9c5db199SXin Li
44*9c5db199SXin Li
45*9c5db199SXin Lidef parse_tko_status_string(status_string):
46*9c5db199SXin Li    """Parse a status string from TKO or the HQE databases.
47*9c5db199SXin Li
48*9c5db199SXin Li    @param status_string: A status string from TKO or HQE databases.
49*9c5db199SXin Li
50*9c5db199SXin Li    @return A status string suitable for inclusion within Cloud Datastore.
51*9c5db199SXin Li    """
52*9c5db199SXin Li    return TKO_STATUS_MAP.get(status_string, 'unknown:' + status_string)
53*9c5db199SXin Li
54*9c5db199SXin Li
55*9c5db199SXin Lidef make_entry(entry_id, name, status, start_time,
56*9c5db199SXin Li               finish_time=None, parent=None):
57*9c5db199SXin Li    """Generate an event log entry to be stored in Cloud Datastore.
58*9c5db199SXin Li
59*9c5db199SXin Li    @param entry_id: A (Kind, id) tuple representing the key.
60*9c5db199SXin Li    @param name: A string identifying the event
61*9c5db199SXin Li    @param status: A string identifying the status of the event.
62*9c5db199SXin Li    @param start_time: A unix timestamp of the start of the event.
63*9c5db199SXin Li    @param finish_time: A unix timestamp of the finish of the event.
64*9c5db199SXin Li    @param parent: A (Kind, id) tuple representing the parent key.
65*9c5db199SXin Li
66*9c5db199SXin Li    @return A dictionary representing the entry suitable for dumping via JSON.
67*9c5db199SXin Li    """
68*9c5db199SXin Li    entry = {
69*9c5db199SXin Li        'id': entry_id,
70*9c5db199SXin Li        'name': name,
71*9c5db199SXin Li        'status': status,
72*9c5db199SXin Li        'start_time': start_time,
73*9c5db199SXin Li    }
74*9c5db199SXin Li    if finish_time is not None:
75*9c5db199SXin Li        entry['finish_time'] = finish_time
76*9c5db199SXin Li    if parent is not None:
77*9c5db199SXin Li        entry['parent'] = parent
78*9c5db199SXin Li    return entry
79*9c5db199SXin Li
80*9c5db199SXin Li
81*9c5db199SXin Lidef find_start_finish_times(statuses):
82*9c5db199SXin Li    """Determines the start and finish times for a list of statuses.
83*9c5db199SXin Li
84*9c5db199SXin Li    @param statuses: A list of job test statuses.
85*9c5db199SXin Li
86*9c5db199SXin Li    @return (start_tme, finish_time) tuple of seconds past epoch.  If either
87*9c5db199SXin Li            cannot be determined, None for that time.
88*9c5db199SXin Li    """
89*9c5db199SXin Li    starts = {to_epoch_time_int(s.test_started_time)
90*9c5db199SXin Li              for s in statuses if s.test_started_time != 'None'}
91*9c5db199SXin Li    finishes = {to_epoch_time_int(s.test_finished_time)
92*9c5db199SXin Li                for s in statuses if s.test_finished_time != 'None'}
93*9c5db199SXin Li    start_time = min(starts) if starts else None
94*9c5db199SXin Li    finish_time = max(finishes) if finishes else None
95*9c5db199SXin Li    return start_time, finish_time
96*9c5db199SXin Li
97*9c5db199SXin Li
98*9c5db199SXin Lidef make_job_entry(tko, job, parent=None, suite_job=False, job_entries=None):
99*9c5db199SXin Li    """Generate a Suite or HWTest event log entry.
100*9c5db199SXin Li
101*9c5db199SXin Li    @param tko: TKO database handle.
102*9c5db199SXin Li    @param job: A frontend.Job to generate an entry for.
103*9c5db199SXin Li    @param parent: A (Kind, id) tuple representing the parent key.
104*9c5db199SXin Li    @param suite_job: A boolean indicating wheret this represents a suite job.
105*9c5db199SXin Li    @param job_entries: A dictionary mapping job id to earlier job entries.
106*9c5db199SXin Li
107*9c5db199SXin Li    @return A dictionary representing the entry suitable for dumping via JSON.
108*9c5db199SXin Li    """
109*9c5db199SXin Li    statuses = tko.get_job_test_statuses_from_db(job.id)
110*9c5db199SXin Li    status = 'pass'
111*9c5db199SXin Li    dut = None
112*9c5db199SXin Li    for s in statuses:
113*9c5db199SXin Li        parsed_status = parse_tko_status_string(s.status)
114*9c5db199SXin Li        # TODO: Improve this generation of status.
115*9c5db199SXin Li        if parsed_status != 'pass':
116*9c5db199SXin Li            status = parsed_status
117*9c5db199SXin Li        if s.hostname:
118*9c5db199SXin Li            dut = s.hostname
119*9c5db199SXin Li        if s.test_started_time == 'None' or s.test_finished_time == 'None':
120*9c5db199SXin Li            logging.warning('TKO entry for %d missing time: %s' % (job.id, str(s)))
121*9c5db199SXin Li    start_time, finish_time = find_start_finish_times(statuses)
122*9c5db199SXin Li    entry = make_entry(('Suite' if suite_job else 'HWTest', int(job.id)),
123*9c5db199SXin Li                       job.name.split('/')[-1], status, start_time,
124*9c5db199SXin Li                       finish_time=finish_time, parent=parent)
125*9c5db199SXin Li
126*9c5db199SXin Li    entry['job_id'] = int(job.id)
127*9c5db199SXin Li    if dut:
128*9c5db199SXin Li        entry['dut'] = dut
129*9c5db199SXin Li    if job.shard:
130*9c5db199SXin Li        entry['shard'] = job.shard
131*9c5db199SXin Li    # Determine the try of this job by looking back through what the
132*9c5db199SXin Li    # original job id is.
133*9c5db199SXin Li    if 'retry_original_job_id' in job.keyvals:
134*9c5db199SXin Li        original_job_id = int(job.keyvals['retry_original_job_id'])
135*9c5db199SXin Li        original_job = job_entries.get(original_job_id, None)
136*9c5db199SXin Li        if original_job:
137*9c5db199SXin Li            entry['try'] = original_job['try'] + 1
138*9c5db199SXin Li        else:
139*9c5db199SXin Li            entry['try'] = 0
140*9c5db199SXin Li    else:
141*9c5db199SXin Li        entry['try'] = 1
142*9c5db199SXin Li    entry['gs_url'] = status_history.get_job_gs_url(job)
143*9c5db199SXin Li    return entry
144*9c5db199SXin Li
145*9c5db199SXin Li
146*9c5db199SXin Lidef make_hqe_entry(hostname, hqe, hqe_statuses, parent=None):
147*9c5db199SXin Li    """Generate a HQE event log entry.
148*9c5db199SXin Li
149*9c5db199SXin Li    @param hostname: A string of the hostname.
150*9c5db199SXin Li    @param hqe: A host history to generate an event for.
151*9c5db199SXin Li    @param hqe_statuses: A dictionary mapping HQE ids to job status.
152*9c5db199SXin Li    @param parent: A (Kind, id) tuple representing the parent key.
153*9c5db199SXin Li
154*9c5db199SXin Li    @return A dictionary representing the entry suitable for dumping via JSON.
155*9c5db199SXin Li    """
156*9c5db199SXin Li    entry = make_entry(
157*9c5db199SXin Li        ('HQE', int(hqe.id)), hostname,
158*9c5db199SXin Li        hqe_statuses.get(hqe.id, parse_tko_status_string(hqe.job_status)),
159*9c5db199SXin Li        hqe.start_time, finish_time=hqe.end_time, parent=parent)
160*9c5db199SXin Li
161*9c5db199SXin Li    entry['task_name'] = hqe.name.split('/')[-1]
162*9c5db199SXin Li    entry['in_suite'] = hqe.id in hqe_statuses
163*9c5db199SXin Li    entry['job_url'] = hqe.job_url
164*9c5db199SXin Li    entry['gs_url'] = hqe.gs_url
165*9c5db199SXin Li    if hqe.job_id is not None:
166*9c5db199SXin Li        entry['job_id'] = hqe.job_id
167*9c5db199SXin Li    entry['is_special'] = hqe.is_special
168*9c5db199SXin Li    return entry
169*9c5db199SXin Li
170*9c5db199SXin Li
171*9c5db199SXin Lidef generate_suite_report(suite_job_id, afe=None, tko=None,
172*9c5db199SXin Li                          reset_finish_time=False):
173*9c5db199SXin Li    """Generate a list of events corresonding to a single suite job.
174*9c5db199SXin Li
175*9c5db199SXin Li    @param suite_job_id: The AFE id of the suite job.
176*9c5db199SXin Li    @param afe: AFE database handle.
177*9c5db199SXin Li    @param tko: TKO database handle.
178*9c5db199SXin Li    @reset_finish_time: Boolean indicating whether to reset the suite finish
179*9c5db199SXin Li                        to now.
180*9c5db199SXin Li
181*9c5db199SXin Li    @return A list of entries suitable for dumping via JSON.
182*9c5db199SXin Li    """
183*9c5db199SXin Li    if afe is None:
184*9c5db199SXin Li        afe = frontend.AFE()
185*9c5db199SXin Li    if tko is None:
186*9c5db199SXin Li        tko = frontend.TKO()
187*9c5db199SXin Li
188*9c5db199SXin Li    # Retrieve the main suite job.
189*9c5db199SXin Li    suite_job = afe.get_jobs(id=suite_job_id)[0]
190*9c5db199SXin Li
191*9c5db199SXin Li    suite_entry = make_job_entry(tko, suite_job, suite_job=True)
192*9c5db199SXin Li    entries = [suite_entry]
193*9c5db199SXin Li
194*9c5db199SXin Li    # Retrieve the child jobs and cache all their statuses
195*9c5db199SXin Li    logging.debug('Fetching child jobs...')
196*9c5db199SXin Li    child_jobs = afe.get_jobs(parent_job_id=suite_job_id)
197*9c5db199SXin Li    logging.debug('... fetched %s child jobs.' % len(child_jobs))
198*9c5db199SXin Li    job_statuses = {}
199*9c5db199SXin Li    job_entries = {}
200*9c5db199SXin Li    for j in child_jobs:
201*9c5db199SXin Li        job_entry = make_job_entry(tko, j, suite_entry['id'],
202*9c5db199SXin Li                                   job_entries=job_entries)
203*9c5db199SXin Li        entries.append(job_entry)
204*9c5db199SXin Li        job_statuses[j.id] = job_entry['status']
205*9c5db199SXin Li        job_entries[j.id] = job_entry
206*9c5db199SXin Li
207*9c5db199SXin Li    # Retrieve the HQEs from all the child jobs, record statuses from
208*9c5db199SXin Li    # job statuses.
209*9c5db199SXin Li    child_job_ids = {j.id for j in child_jobs}
210*9c5db199SXin Li    logging.debug('Fetching HQEs...')
211*9c5db199SXin Li    hqes = afe.get_host_queue_entries(job_id__in=list(child_job_ids))
212*9c5db199SXin Li    logging.debug('... fetched %s HQEs.' % len(hqes))
213*9c5db199SXin Li    hqe_statuses = {h.id: job_statuses.get(h.job.id, None) for h in hqes}
214*9c5db199SXin Li
215*9c5db199SXin Li    # Generate list of hosts.
216*9c5db199SXin Li    hostnames = {h.host.hostname for h in hqes if h.host}
217*9c5db199SXin Li    logging.debug('%s distinct hosts participated in the suite.' %
218*9c5db199SXin Li                  len(hostnames))
219*9c5db199SXin Li
220*9c5db199SXin Li    suite_start_time = suite_entry.get('start_time')
221*9c5db199SXin Li    suite_finish_time = suite_entry.get('finish_time')
222*9c5db199SXin Li    # Retrieve histories for the time of the suite for all associated hosts.
223*9c5db199SXin Li    # TODO: Include all hosts in the pool.
224*9c5db199SXin Li    if suite_start_time and suite_finish_time:
225*9c5db199SXin Li
226*9c5db199SXin Li        if reset_finish_time:
227*9c5db199SXin Li            suite_timeout_time = suite_start_time + DEFAULT_SUITE_TIMEOUT
228*9c5db199SXin Li            current_time = to_epoch_time_int(datetime.datetime.now())
229*9c5db199SXin Li            suite_finish_time = min(current_time, suite_timeout_time)
230*9c5db199SXin Li
231*9c5db199SXin Li        histories = [HostJobHistory.get_host_history(afe, hostname,
232*9c5db199SXin Li                                                     suite_start_time,
233*9c5db199SXin Li                                                     suite_finish_time)
234*9c5db199SXin Li                     for hostname in sorted(hostnames)]
235*9c5db199SXin Li
236*9c5db199SXin Li        for history in histories:
237*9c5db199SXin Li            entries.extend(make_hqe_entry(history.hostname, h, hqe_statuses,
238*9c5db199SXin Li                                          suite_entry['id']) for h in history)
239*9c5db199SXin Li
240*9c5db199SXin Li    return entries
241*9c5db199SXin Li
242*9c5db199SXin Lidef dump_entries_as_json(entries, output_file):
243*9c5db199SXin Li    """Dump event log entries as json to a file.
244*9c5db199SXin Li
245*9c5db199SXin Li    @param entries: A list of event log entries to dump.
246*9c5db199SXin Li    @param output_file: The file to write to.
247*9c5db199SXin Li    """
248*9c5db199SXin Li    # Write the entries out as JSON.
249*9c5db199SXin Li    logging.debug('Dumping %d entries' % len(entries))
250*9c5db199SXin Li    for e in entries:
251*9c5db199SXin Li        json.dump(e, output_file, sort_keys=True)
252*9c5db199SXin Li        output_file.write('\n')
253