1*4882a593Smuzhiyun# test result tool - report text based test results 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# Copyright (c) 2019, Intel Corporation. 4*4882a593Smuzhiyun# Copyright (c) 2019, Linux Foundation 5*4882a593Smuzhiyun# 6*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 7*4882a593Smuzhiyun# 8*4882a593Smuzhiyun 9*4882a593Smuzhiyunimport os 10*4882a593Smuzhiyunimport glob 11*4882a593Smuzhiyunimport json 12*4882a593Smuzhiyunimport resulttool.resultutils as resultutils 13*4882a593Smuzhiyunfrom oeqa.utils.git import GitRepo 14*4882a593Smuzhiyunimport oeqa.utils.gitarchive as gitarchive 15*4882a593Smuzhiyun 16*4882a593Smuzhiyun 17*4882a593Smuzhiyunclass ResultsTextReport(object): 18*4882a593Smuzhiyun def __init__(self): 19*4882a593Smuzhiyun self.ptests = {} 20*4882a593Smuzhiyun self.ltptests = {} 21*4882a593Smuzhiyun self.ltpposixtests = {} 22*4882a593Smuzhiyun self.result_types = {'passed': ['PASSED', 'passed', 'PASS', 'XFAIL'], 23*4882a593Smuzhiyun 'failed': ['FAILED', 'failed', 'FAIL', 'ERROR', 'error', 'UNKNOWN', 'XPASS'], 24*4882a593Smuzhiyun 'skipped': ['SKIPPED', 'skipped', 'UNSUPPORTED', 'UNTESTED', 'UNRESOLVED']} 25*4882a593Smuzhiyun 26*4882a593Smuzhiyun 27*4882a593Smuzhiyun def handle_ptest_result(self, k, status, result, machine): 28*4882a593Smuzhiyun if machine not in self.ptests: 29*4882a593Smuzhiyun self.ptests[machine] = {} 30*4882a593Smuzhiyun 31*4882a593Smuzhiyun if k == 'ptestresult.sections': 32*4882a593Smuzhiyun # Ensure tests without any test results still show up on the report 33*4882a593Smuzhiyun for suite in result['ptestresult.sections']: 34*4882a593Smuzhiyun if suite not in self.ptests[machine]: 35*4882a593Smuzhiyun self.ptests[machine][suite] = { 36*4882a593Smuzhiyun 'passed': 0, 'failed': 0, 'skipped': 0, 'duration' : '-', 37*4882a593Smuzhiyun 'failed_testcases': [], "testcases": set(), 38*4882a593Smuzhiyun } 39*4882a593Smuzhiyun if 'duration' in result['ptestresult.sections'][suite]: 40*4882a593Smuzhiyun self.ptests[machine][suite]['duration'] = result['ptestresult.sections'][suite]['duration'] 41*4882a593Smuzhiyun if 'timeout' in result['ptestresult.sections'][suite]: 42*4882a593Smuzhiyun self.ptests[machine][suite]['duration'] += " T" 43*4882a593Smuzhiyun return True 44*4882a593Smuzhiyun 45*4882a593Smuzhiyun # process test result 46*4882a593Smuzhiyun try: 47*4882a593Smuzhiyun _, suite, test = k.split(".", 2) 48*4882a593Smuzhiyun except ValueError: 49*4882a593Smuzhiyun return True 50*4882a593Smuzhiyun 51*4882a593Smuzhiyun # Handle 'glib-2.0' 52*4882a593Smuzhiyun if 'ptestresult.sections' in result and suite not in result['ptestresult.sections']: 53*4882a593Smuzhiyun try: 54*4882a593Smuzhiyun _, suite, suite1, test = k.split(".", 3) 55*4882a593Smuzhiyun if suite + "." + suite1 in result['ptestresult.sections']: 56*4882a593Smuzhiyun suite = suite + "." + suite1 57*4882a593Smuzhiyun except ValueError: 58*4882a593Smuzhiyun pass 59*4882a593Smuzhiyun 60*4882a593Smuzhiyun if suite not in self.ptests[machine]: 61*4882a593Smuzhiyun self.ptests[machine][suite] = { 62*4882a593Smuzhiyun 'passed': 0, 'failed': 0, 'skipped': 0, 'duration' : '-', 63*4882a593Smuzhiyun 'failed_testcases': [], "testcases": set(), 64*4882a593Smuzhiyun } 65*4882a593Smuzhiyun 66*4882a593Smuzhiyun # do not process duplicate results 67*4882a593Smuzhiyun if test in self.ptests[machine][suite]["testcases"]: 68*4882a593Smuzhiyun print("Warning duplicate ptest result '{}.{}' for {}".format(suite, test, machine)) 69*4882a593Smuzhiyun return False 70*4882a593Smuzhiyun 71*4882a593Smuzhiyun for tk in self.result_types: 72*4882a593Smuzhiyun if status in self.result_types[tk]: 73*4882a593Smuzhiyun self.ptests[machine][suite][tk] += 1 74*4882a593Smuzhiyun self.ptests[machine][suite]["testcases"].add(test) 75*4882a593Smuzhiyun return True 76*4882a593Smuzhiyun 77*4882a593Smuzhiyun def handle_ltptest_result(self, k, status, result, machine): 78*4882a593Smuzhiyun if machine not in self.ltptests: 79*4882a593Smuzhiyun self.ltptests[machine] = {} 80*4882a593Smuzhiyun 81*4882a593Smuzhiyun if k == 'ltpresult.sections': 82*4882a593Smuzhiyun # Ensure tests without any test results still show up on the report 83*4882a593Smuzhiyun for suite in result['ltpresult.sections']: 84*4882a593Smuzhiyun if suite not in self.ltptests[machine]: 85*4882a593Smuzhiyun self.ltptests[machine][suite] = {'passed': 0, 'failed': 0, 'skipped': 0, 'duration' : '-', 'failed_testcases': []} 86*4882a593Smuzhiyun if 'duration' in result['ltpresult.sections'][suite]: 87*4882a593Smuzhiyun self.ltptests[machine][suite]['duration'] = result['ltpresult.sections'][suite]['duration'] 88*4882a593Smuzhiyun if 'timeout' in result['ltpresult.sections'][suite]: 89*4882a593Smuzhiyun self.ltptests[machine][suite]['duration'] += " T" 90*4882a593Smuzhiyun return 91*4882a593Smuzhiyun try: 92*4882a593Smuzhiyun _, suite, test = k.split(".", 2) 93*4882a593Smuzhiyun except ValueError: 94*4882a593Smuzhiyun return 95*4882a593Smuzhiyun # Handle 'glib-2.0' 96*4882a593Smuzhiyun if 'ltpresult.sections' in result and suite not in result['ltpresult.sections']: 97*4882a593Smuzhiyun try: 98*4882a593Smuzhiyun _, suite, suite1, test = k.split(".", 3) 99*4882a593Smuzhiyun if suite + "." + suite1 in result['ltpresult.sections']: 100*4882a593Smuzhiyun suite = suite + "." + suite1 101*4882a593Smuzhiyun except ValueError: 102*4882a593Smuzhiyun pass 103*4882a593Smuzhiyun if suite not in self.ltptests[machine]: 104*4882a593Smuzhiyun self.ltptests[machine][suite] = {'passed': 0, 'failed': 0, 'skipped': 0, 'duration' : '-', 'failed_testcases': []} 105*4882a593Smuzhiyun for tk in self.result_types: 106*4882a593Smuzhiyun if status in self.result_types[tk]: 107*4882a593Smuzhiyun self.ltptests[machine][suite][tk] += 1 108*4882a593Smuzhiyun 109*4882a593Smuzhiyun def handle_ltpposixtest_result(self, k, status, result, machine): 110*4882a593Smuzhiyun if machine not in self.ltpposixtests: 111*4882a593Smuzhiyun self.ltpposixtests[machine] = {} 112*4882a593Smuzhiyun 113*4882a593Smuzhiyun if k == 'ltpposixresult.sections': 114*4882a593Smuzhiyun # Ensure tests without any test results still show up on the report 115*4882a593Smuzhiyun for suite in result['ltpposixresult.sections']: 116*4882a593Smuzhiyun if suite not in self.ltpposixtests[machine]: 117*4882a593Smuzhiyun self.ltpposixtests[machine][suite] = {'passed': 0, 'failed': 0, 'skipped': 0, 'duration' : '-', 'failed_testcases': []} 118*4882a593Smuzhiyun if 'duration' in result['ltpposixresult.sections'][suite]: 119*4882a593Smuzhiyun self.ltpposixtests[machine][suite]['duration'] = result['ltpposixresult.sections'][suite]['duration'] 120*4882a593Smuzhiyun return 121*4882a593Smuzhiyun try: 122*4882a593Smuzhiyun _, suite, test = k.split(".", 2) 123*4882a593Smuzhiyun except ValueError: 124*4882a593Smuzhiyun return 125*4882a593Smuzhiyun # Handle 'glib-2.0' 126*4882a593Smuzhiyun if 'ltpposixresult.sections' in result and suite not in result['ltpposixresult.sections']: 127*4882a593Smuzhiyun try: 128*4882a593Smuzhiyun _, suite, suite1, test = k.split(".", 3) 129*4882a593Smuzhiyun if suite + "." + suite1 in result['ltpposixresult.sections']: 130*4882a593Smuzhiyun suite = suite + "." + suite1 131*4882a593Smuzhiyun except ValueError: 132*4882a593Smuzhiyun pass 133*4882a593Smuzhiyun if suite not in self.ltpposixtests[machine]: 134*4882a593Smuzhiyun self.ltpposixtests[machine][suite] = {'passed': 0, 'failed': 0, 'skipped': 0, 'duration' : '-', 'failed_testcases': []} 135*4882a593Smuzhiyun for tk in self.result_types: 136*4882a593Smuzhiyun if status in self.result_types[tk]: 137*4882a593Smuzhiyun self.ltpposixtests[machine][suite][tk] += 1 138*4882a593Smuzhiyun 139*4882a593Smuzhiyun def get_aggregated_test_result(self, logger, testresult, machine): 140*4882a593Smuzhiyun test_count_report = {'passed': 0, 'failed': 0, 'skipped': 0, 'failed_testcases': []} 141*4882a593Smuzhiyun result = testresult.get('result', []) 142*4882a593Smuzhiyun for k in result: 143*4882a593Smuzhiyun test_status = result[k].get('status', []) 144*4882a593Smuzhiyun if k.startswith("ptestresult."): 145*4882a593Smuzhiyun if not self.handle_ptest_result(k, test_status, result, machine): 146*4882a593Smuzhiyun continue 147*4882a593Smuzhiyun elif k.startswith("ltpresult."): 148*4882a593Smuzhiyun self.handle_ltptest_result(k, test_status, result, machine) 149*4882a593Smuzhiyun elif k.startswith("ltpposixresult."): 150*4882a593Smuzhiyun self.handle_ltpposixtest_result(k, test_status, result, machine) 151*4882a593Smuzhiyun 152*4882a593Smuzhiyun # process result if it was not skipped by a handler 153*4882a593Smuzhiyun for tk in self.result_types: 154*4882a593Smuzhiyun if test_status in self.result_types[tk]: 155*4882a593Smuzhiyun test_count_report[tk] += 1 156*4882a593Smuzhiyun if test_status in self.result_types['failed']: 157*4882a593Smuzhiyun test_count_report['failed_testcases'].append(k) 158*4882a593Smuzhiyun return test_count_report 159*4882a593Smuzhiyun 160*4882a593Smuzhiyun def print_test_report(self, template_file_name, test_count_reports): 161*4882a593Smuzhiyun from jinja2 import Environment, FileSystemLoader 162*4882a593Smuzhiyun script_path = os.path.dirname(os.path.realpath(__file__)) 163*4882a593Smuzhiyun file_loader = FileSystemLoader(script_path + '/template') 164*4882a593Smuzhiyun env = Environment(loader=file_loader, trim_blocks=True) 165*4882a593Smuzhiyun template = env.get_template(template_file_name) 166*4882a593Smuzhiyun havefailed = False 167*4882a593Smuzhiyun reportvalues = [] 168*4882a593Smuzhiyun machines = [] 169*4882a593Smuzhiyun cols = ['passed', 'failed', 'skipped'] 170*4882a593Smuzhiyun maxlen = {'passed' : 0, 'failed' : 0, 'skipped' : 0, 'result_id': 0, 'testseries' : 0, 'ptest' : 0 ,'ltptest': 0, 'ltpposixtest': 0} 171*4882a593Smuzhiyun for line in test_count_reports: 172*4882a593Smuzhiyun total_tested = line['passed'] + line['failed'] + line['skipped'] 173*4882a593Smuzhiyun vals = {} 174*4882a593Smuzhiyun vals['result_id'] = line['result_id'] 175*4882a593Smuzhiyun vals['testseries'] = line['testseries'] 176*4882a593Smuzhiyun vals['sort'] = line['testseries'] + "_" + line['result_id'] 177*4882a593Smuzhiyun vals['failed_testcases'] = line['failed_testcases'] 178*4882a593Smuzhiyun for k in cols: 179*4882a593Smuzhiyun vals[k] = "%d (%s%%)" % (line[k], format(line[k] / total_tested * 100, '.0f')) 180*4882a593Smuzhiyun for k in maxlen: 181*4882a593Smuzhiyun if k in vals and len(vals[k]) > maxlen[k]: 182*4882a593Smuzhiyun maxlen[k] = len(vals[k]) 183*4882a593Smuzhiyun reportvalues.append(vals) 184*4882a593Smuzhiyun if line['failed_testcases']: 185*4882a593Smuzhiyun havefailed = True 186*4882a593Smuzhiyun if line['machine'] not in machines: 187*4882a593Smuzhiyun machines.append(line['machine']) 188*4882a593Smuzhiyun reporttotalvalues = {} 189*4882a593Smuzhiyun for k in cols: 190*4882a593Smuzhiyun reporttotalvalues[k] = '%s' % sum([line[k] for line in test_count_reports]) 191*4882a593Smuzhiyun reporttotalvalues['count'] = '%s' % len(test_count_reports) 192*4882a593Smuzhiyun for (machine, report) in self.ptests.items(): 193*4882a593Smuzhiyun for ptest in self.ptests[machine]: 194*4882a593Smuzhiyun if len(ptest) > maxlen['ptest']: 195*4882a593Smuzhiyun maxlen['ptest'] = len(ptest) 196*4882a593Smuzhiyun for (machine, report) in self.ltptests.items(): 197*4882a593Smuzhiyun for ltptest in self.ltptests[machine]: 198*4882a593Smuzhiyun if len(ltptest) > maxlen['ltptest']: 199*4882a593Smuzhiyun maxlen['ltptest'] = len(ltptest) 200*4882a593Smuzhiyun for (machine, report) in self.ltpposixtests.items(): 201*4882a593Smuzhiyun for ltpposixtest in self.ltpposixtests[machine]: 202*4882a593Smuzhiyun if len(ltpposixtest) > maxlen['ltpposixtest']: 203*4882a593Smuzhiyun maxlen['ltpposixtest'] = len(ltpposixtest) 204*4882a593Smuzhiyun output = template.render(reportvalues=reportvalues, 205*4882a593Smuzhiyun reporttotalvalues=reporttotalvalues, 206*4882a593Smuzhiyun havefailed=havefailed, 207*4882a593Smuzhiyun machines=machines, 208*4882a593Smuzhiyun ptests=self.ptests, 209*4882a593Smuzhiyun ltptests=self.ltptests, 210*4882a593Smuzhiyun ltpposixtests=self.ltpposixtests, 211*4882a593Smuzhiyun maxlen=maxlen) 212*4882a593Smuzhiyun print(output) 213*4882a593Smuzhiyun 214*4882a593Smuzhiyun def view_test_report(self, logger, source_dir, branch, commit, tag, use_regression_map, raw_test, selected_test_case_only): 215*4882a593Smuzhiyun def print_selected_testcase_result(testresults, selected_test_case_only): 216*4882a593Smuzhiyun for testsuite in testresults: 217*4882a593Smuzhiyun for resultid in testresults[testsuite]: 218*4882a593Smuzhiyun result = testresults[testsuite][resultid]['result'] 219*4882a593Smuzhiyun test_case_result = result.get(selected_test_case_only, {}) 220*4882a593Smuzhiyun if test_case_result.get('status'): 221*4882a593Smuzhiyun print('Found selected test case result for %s from %s' % (selected_test_case_only, 222*4882a593Smuzhiyun resultid)) 223*4882a593Smuzhiyun print(test_case_result['status']) 224*4882a593Smuzhiyun else: 225*4882a593Smuzhiyun print('Could not find selected test case result for %s from %s' % (selected_test_case_only, 226*4882a593Smuzhiyun resultid)) 227*4882a593Smuzhiyun if test_case_result.get('log'): 228*4882a593Smuzhiyun print(test_case_result['log']) 229*4882a593Smuzhiyun test_count_reports = [] 230*4882a593Smuzhiyun configmap = resultutils.store_map 231*4882a593Smuzhiyun if use_regression_map: 232*4882a593Smuzhiyun configmap = resultutils.regression_map 233*4882a593Smuzhiyun if commit: 234*4882a593Smuzhiyun if tag: 235*4882a593Smuzhiyun logger.warning("Ignoring --tag as --commit was specified") 236*4882a593Smuzhiyun tag_name = "{branch}/{commit_number}-g{commit}/{tag_number}" 237*4882a593Smuzhiyun repo = GitRepo(source_dir) 238*4882a593Smuzhiyun revs = gitarchive.get_test_revs(logger, repo, tag_name, branch=branch) 239*4882a593Smuzhiyun rev_index = gitarchive.rev_find(revs, 'commit', commit) 240*4882a593Smuzhiyun testresults = resultutils.git_get_result(repo, revs[rev_index][2], configmap=configmap) 241*4882a593Smuzhiyun elif tag: 242*4882a593Smuzhiyun repo = GitRepo(source_dir) 243*4882a593Smuzhiyun testresults = resultutils.git_get_result(repo, [tag], configmap=configmap) 244*4882a593Smuzhiyun else: 245*4882a593Smuzhiyun testresults = resultutils.load_resultsdata(source_dir, configmap=configmap) 246*4882a593Smuzhiyun if raw_test: 247*4882a593Smuzhiyun raw_results = {} 248*4882a593Smuzhiyun for testsuite in testresults: 249*4882a593Smuzhiyun result = testresults[testsuite].get(raw_test, {}) 250*4882a593Smuzhiyun if result: 251*4882a593Smuzhiyun raw_results[testsuite] = {raw_test: result} 252*4882a593Smuzhiyun if raw_results: 253*4882a593Smuzhiyun if selected_test_case_only: 254*4882a593Smuzhiyun print_selected_testcase_result(raw_results, selected_test_case_only) 255*4882a593Smuzhiyun else: 256*4882a593Smuzhiyun print(json.dumps(raw_results, sort_keys=True, indent=4)) 257*4882a593Smuzhiyun else: 258*4882a593Smuzhiyun print('Could not find raw test result for %s' % raw_test) 259*4882a593Smuzhiyun return 0 260*4882a593Smuzhiyun if selected_test_case_only: 261*4882a593Smuzhiyun print_selected_testcase_result(testresults, selected_test_case_only) 262*4882a593Smuzhiyun return 0 263*4882a593Smuzhiyun for testsuite in testresults: 264*4882a593Smuzhiyun for resultid in testresults[testsuite]: 265*4882a593Smuzhiyun skip = False 266*4882a593Smuzhiyun result = testresults[testsuite][resultid] 267*4882a593Smuzhiyun machine = result['configuration']['MACHINE'] 268*4882a593Smuzhiyun 269*4882a593Smuzhiyun # Check to see if there is already results for these kinds of tests for the machine 270*4882a593Smuzhiyun for key in result['result'].keys(): 271*4882a593Smuzhiyun testtype = str(key).split('.')[0] 272*4882a593Smuzhiyun if ((machine in self.ltptests and testtype == "ltpiresult" and self.ltptests[machine]) or 273*4882a593Smuzhiyun (machine in self.ltpposixtests and testtype == "ltpposixresult" and self.ltpposixtests[machine])): 274*4882a593Smuzhiyun print("Already have test results for %s on %s, skipping %s" %(str(key).split('.')[0], machine, resultid)) 275*4882a593Smuzhiyun skip = True 276*4882a593Smuzhiyun break 277*4882a593Smuzhiyun if skip: 278*4882a593Smuzhiyun break 279*4882a593Smuzhiyun 280*4882a593Smuzhiyun test_count_report = self.get_aggregated_test_result(logger, result, machine) 281*4882a593Smuzhiyun test_count_report['machine'] = machine 282*4882a593Smuzhiyun test_count_report['testseries'] = result['configuration']['TESTSERIES'] 283*4882a593Smuzhiyun test_count_report['result_id'] = resultid 284*4882a593Smuzhiyun test_count_reports.append(test_count_report) 285*4882a593Smuzhiyun self.print_test_report('test_report_full_text.txt', test_count_reports) 286*4882a593Smuzhiyun 287*4882a593Smuzhiyundef report(args, logger): 288*4882a593Smuzhiyun report = ResultsTextReport() 289*4882a593Smuzhiyun report.view_test_report(logger, args.source_dir, args.branch, args.commit, args.tag, args.use_regression_map, 290*4882a593Smuzhiyun args.raw_test_only, args.selected_test_case_only) 291*4882a593Smuzhiyun return 0 292*4882a593Smuzhiyun 293*4882a593Smuzhiyundef register_commands(subparsers): 294*4882a593Smuzhiyun """Register subcommands from this plugin""" 295*4882a593Smuzhiyun parser_build = subparsers.add_parser('report', help='summarise test results', 296*4882a593Smuzhiyun description='print a text-based summary of the test results', 297*4882a593Smuzhiyun group='analysis') 298*4882a593Smuzhiyun parser_build.set_defaults(func=report) 299*4882a593Smuzhiyun parser_build.add_argument('source_dir', 300*4882a593Smuzhiyun help='source file/directory/URL that contain the test result files to summarise') 301*4882a593Smuzhiyun parser_build.add_argument('--branch', '-B', default='master', help="Branch to find commit in") 302*4882a593Smuzhiyun parser_build.add_argument('--commit', help="Revision to report") 303*4882a593Smuzhiyun parser_build.add_argument('-t', '--tag', default='', 304*4882a593Smuzhiyun help='source_dir is a git repository, report on the tag specified from that repository') 305*4882a593Smuzhiyun parser_build.add_argument('-m', '--use_regression_map', action='store_true', 306*4882a593Smuzhiyun help='instead of the default "store_map", use the "regression_map" for report') 307*4882a593Smuzhiyun parser_build.add_argument('-r', '--raw_test_only', default='', 308*4882a593Smuzhiyun help='output raw test result only for the user provided test result id') 309*4882a593Smuzhiyun parser_build.add_argument('-s', '--selected_test_case_only', default='', 310*4882a593Smuzhiyun help='output selected test case result for the user provided test case id, if both test ' 311*4882a593Smuzhiyun 'result id and test case id are provided then output the selected test case result ' 312*4882a593Smuzhiyun 'from the provided test result id') 313