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