1*9c5db199SXin Li#!/usr/bin/env python3 2*9c5db199SXin Li# -*- coding: utf-8 -*- 3*9c5db199SXin Li# Copyright 2021 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 Liimport argparse 8*9c5db199SXin Liimport ast 9*9c5db199SXin Lifrom functools import partial 10*9c5db199SXin Liimport os 11*9c5db199SXin Liimport subprocess 12*9c5db199SXin Liimport graphviz 13*9c5db199SXin Liimport common 14*9c5db199SXin Li 15*9c5db199SXin Lifrom server.cros.dynamic_suite.control_file_getter import FileSystemGetter 16*9c5db199SXin Lifrom server.cros.dynamic_suite.suite_common import retrieve_for_suite 17*9c5db199SXin Li 18*9c5db199SXin Liclass TestSuite(object): 19*9c5db199SXin Li def __init__(self, cf_object, name, file_path): 20*9c5db199SXin Li self.name = name 21*9c5db199SXin Li self.cf_object = cf_object 22*9c5db199SXin Li self.tests = [] 23*9c5db199SXin Li self.file_path = file_path 24*9c5db199SXin Li 25*9c5db199SXin Li def add_test(self, test_object): 26*9c5db199SXin Li self.tests.append(test_object) 27*9c5db199SXin Li 28*9c5db199SXin Li def get_tests(self): 29*9c5db199SXin Li return self.tests 30*9c5db199SXin Li 31*9c5db199SXin Li 32*9c5db199SXin Liclass TestObject(object): 33*9c5db199SXin Li def __init__(self, cf_object, file_path): 34*9c5db199SXin Li self.name = cf_object.name 35*9c5db199SXin Li self.type = 'tast' if ('tast' in self.name) else 'tauto' 36*9c5db199SXin Li self.cf_object = cf_object 37*9c5db199SXin Li self.file_path = file_path 38*9c5db199SXin Li self.tast_exprs = '' 39*9c5db199SXin Li self.tast_string = '' 40*9c5db199SXin Li 41*9c5db199SXin Li def get_attributes(self): 42*9c5db199SXin Li return self.cf_object.attributes 43*9c5db199SXin Li 44*9c5db199SXin Li def is_tast(self): 45*9c5db199SXin Li return self.type == 'tast' 46*9c5db199SXin Li 47*9c5db199SXin Li # use the python syntax tree library to parse the run function 48*9c5db199SXin Li # and grab the test_expr from the 'tast' run command 49*9c5db199SXin Li def parse_cf_for_tast_string(self): 50*9c5db199SXin Li with open(self.file_path, 'r') as cf: 51*9c5db199SXin Li mod = ast.parse(cf.read()) 52*9c5db199SXin Li for n in mod.body: 53*9c5db199SXin Li if n.__class__ != ast.FunctionDef: 54*9c5db199SXin Li continue 55*9c5db199SXin Li if n.name != 'run': 56*9c5db199SXin Li continue 57*9c5db199SXin Li for sub_node in n.body: 58*9c5db199SXin Li if sub_node.__class__ != ast.Expr: 59*9c5db199SXin Li continue 60*9c5db199SXin Li try: 61*9c5db199SXin Li fn_name = sub_node.value.func.value.id 62*9c5db199SXin Li if fn_name != 'job': 63*9c5db199SXin Li continue 64*9c5db199SXin Li except: 65*9c5db199SXin Li continue 66*9c5db199SXin Li if sub_node.value.func.attr != 'run_test': 67*9c5db199SXin Li continue 68*9c5db199SXin Li for keyword in sub_node.value.keywords: 69*9c5db199SXin Li if keyword.arg == 'test_exprs' and keyword.value.__class__ == ast.List: 70*9c5db199SXin Li test_exprs = [] 71*9c5db199SXin Li regex_list = False 72*9c5db199SXin Li for elem in keyword.value.elts: 73*9c5db199SXin Li try: 74*9c5db199SXin Li test_exprs.append(elem.s) 75*9c5db199SXin Li regex_list = ('(' in elem.s or regex_list) 76*9c5db199SXin Li except AttributeError: 77*9c5db199SXin Li print('WARNING: Non-standard test found, check ' 78*9c5db199SXin Li + self.file_path + ' manually') 79*9c5db199SXin Li break 80*9c5db199SXin Li if regex_list: 81*9c5db199SXin Li self.tast_string = ' '.join(test_exprs) 82*9c5db199SXin Li else: 83*9c5db199SXin Li for it in range(len(test_exprs) - 1): 84*9c5db199SXin Li test_exprs[it] = test_exprs[it] + ',' 85*9c5db199SXin Li self.tast_string = ' '.join(test_exprs) 86*9c5db199SXin Li 87*9c5db199SXin Li def enumerate_tast_from_test_expr(self): 88*9c5db199SXin Li self.parse_cf_for_tast_string() 89*9c5db199SXin Li try: 90*9c5db199SXin Li self.tast_exprs = self.tast_string.split(', ') 91*9c5db199SXin Li except AttributeError: 92*9c5db199SXin Li print('WARNING: Non-standard test found, check' + self.file_path + 93*9c5db199SXin Li ' manually') 94*9c5db199SXin Li 95*9c5db199SXin Li def enumerate_tests_from_tast_exprs(self, dut): 96*9c5db199SXin Li tests = [] 97*9c5db199SXin Li print(self.tast_exprs) 98*9c5db199SXin Li for expr in self.tast_exprs: 99*9c5db199SXin Li en = subprocess.check_output( 100*9c5db199SXin Li ['tast', 'list', str(dut), 101*9c5db199SXin Li str(expr)], encoding='utf-8') 102*9c5db199SXin Li for t in en.split('\n'): 103*9c5db199SXin Li if t == '': 104*9c5db199SXin Li continue 105*9c5db199SXin Li tests.append(t) 106*9c5db199SXin Li en = subprocess.check_output([ 107*9c5db199SXin Li 'tast', 'list', '-buildbundle=crosint', 108*9c5db199SXin Li str(dut), 109*9c5db199SXin Li str(expr) 110*9c5db199SXin Li ], 111*9c5db199SXin Li encoding='utf-8') 112*9c5db199SXin Li for t in en.split('\n'): 113*9c5db199SXin Li if t == '': 114*9c5db199SXin Li continue 115*9c5db199SXin Li tests.append(t) 116*9c5db199SXin Li 117*9c5db199SXin Li return tests 118*9c5db199SXin Li 119*9c5db199SXin Li def describe(self): 120*9c5db199SXin Li return 'test named ' + self.name + ' of type ' + self.type 121*9c5db199SXin Li 122*9c5db199SXin Li 123*9c5db199SXin Liclass TestParser(object): 124*9c5db199SXin Li def get_all_test_objects(self, locations): 125*9c5db199SXin Li tests = {} 126*9c5db199SXin Li suites = {} 127*9c5db199SXin Li 128*9c5db199SXin Li cf_getter = FileSystemGetter(locations) 129*9c5db199SXin Li for (file_path, cf_object) in retrieve_for_suite(cf_getter, 130*9c5db199SXin Li '').items(): 131*9c5db199SXin Li if cf_object.test_class == 'suite': 132*9c5db199SXin Li suites[cf_object.name] = (TestSuite(cf_object, cf_object.name, 133*9c5db199SXin Li file_path)) 134*9c5db199SXin Li else: 135*9c5db199SXin Li tests[cf_object.name] = (TestObject(cf_object, file_path)) 136*9c5db199SXin Li if tests[cf_object.name].is_tast(): 137*9c5db199SXin Li tests[cf_object.name].enumerate_tast_from_test_expr() 138*9c5db199SXin Li 139*9c5db199SXin Li return tests, suites 140*9c5db199SXin Li 141*9c5db199SXin Li 142*9c5db199SXin Liclass TestManager(object): 143*9c5db199SXin Li def __init__(self): 144*9c5db199SXin Li self.tests = {} 145*9c5db199SXin Li self.suites = {} 146*9c5db199SXin Li self.dut = None 147*9c5db199SXin Li self.log_functions = [partial(print)] 148*9c5db199SXin Li self.test_parser = TestParser() 149*9c5db199SXin Li 150*9c5db199SXin Li def log(self, log_text, *args): 151*9c5db199SXin Li for fn in self.log_functions: 152*9c5db199SXin Li fn(log_text, *args) 153*9c5db199SXin Li 154*9c5db199SXin Li def csv_logger(self, log_text, file_path): 155*9c5db199SXin Li with open(file_path, 'a') as log: 156*9c5db199SXin Li log.write(log_text) 157*9c5db199SXin Li 158*9c5db199SXin Li def register_csv_logger(self, file_path): 159*9c5db199SXin Li if os.path.exists(file_path): 160*9c5db199SXin Li os.remove(file_path) 161*9c5db199SXin Li print_to_csv = partial(self.csv_logger, file_path=file_path) 162*9c5db199SXin Li self.log_functions.append(print_to_csv) 163*9c5db199SXin Li print_to_csv('suite,test\n') 164*9c5db199SXin Li 165*9c5db199SXin Li def initialize_from_fs(self, locations): 166*9c5db199SXin Li self.tests, self.suites = self.test_parser.get_all_test_objects( 167*9c5db199SXin Li locations) 168*9c5db199SXin Li 169*9c5db199SXin Li def process_all_tests(self): 170*9c5db199SXin Li for test, test_object in self.tests.items(): 171*9c5db199SXin Li for suite in test_object.get_attributes(): 172*9c5db199SXin Li target_suite = self.find_suite_named(suite) 173*9c5db199SXin Li if target_suite is not None: 174*9c5db199SXin Li target_suite.add_test(test) 175*9c5db199SXin Li 176*9c5db199SXin Li def set_dut(self, target): 177*9c5db199SXin Li self.dut = target 178*9c5db199SXin Li 179*9c5db199SXin Li def get_dut(self): 180*9c5db199SXin Li if self.dut is not None: 181*9c5db199SXin Li return self.dut 182*9c5db199SXin Li else: 183*9c5db199SXin Li raise AttributeError( 184*9c5db199SXin Li 'DUT Address not set, please use the --dut flag to indicate the ip address of the DUT' 185*9c5db199SXin Li ) 186*9c5db199SXin Li 187*9c5db199SXin Li def find_test_named(self, test_name): 188*9c5db199SXin Li try: 189*9c5db199SXin Li queried_test = self.tests[test_name] 190*9c5db199SXin Li return queried_test 191*9c5db199SXin Li except KeyError: 192*9c5db199SXin Li return None 193*9c5db199SXin Li 194*9c5db199SXin Li def find_suite_named(self, suite_name): 195*9c5db199SXin Li try: 196*9c5db199SXin Li if suite_name[0:6] == 'suite.': 197*9c5db199SXin Li queried_suite = self.suites[suite_name[6:]] 198*9c5db199SXin Li elif suite_name[0:6] == 'suite:': 199*9c5db199SXin Li queried_suite = self.suites[suite_name[6:]] 200*9c5db199SXin Li else: 201*9c5db199SXin Li queried_suite = self.suites[suite_name] 202*9c5db199SXin Li return queried_suite 203*9c5db199SXin Li except KeyError: 204*9c5db199SXin Li return None 205*9c5db199SXin Li 206*9c5db199SXin Li def list_suite_named(self, suite_name, pretty=False): 207*9c5db199SXin Li suite_tests = [] 208*9c5db199SXin Li suite = self.find_suite_named(suite_name) 209*9c5db199SXin Li 210*9c5db199SXin Li if suite is None: 211*9c5db199SXin Li if pretty: 212*9c5db199SXin Li return '\n' 213*9c5db199SXin Li return suite_tests 214*9c5db199SXin Li 215*9c5db199SXin Li for test in suite.get_tests(): 216*9c5db199SXin Li if self.tests[test].is_tast(): 217*9c5db199SXin Li found_tests = self.tests[test].enumerate_tests_from_tast_exprs( 218*9c5db199SXin Li self.get_dut()) 219*9c5db199SXin Li for t in found_tests: 220*9c5db199SXin Li if t == '': 221*9c5db199SXin Li continue 222*9c5db199SXin Li suite_tests.append('tast.' + str(t)) 223*9c5db199SXin Li else: 224*9c5db199SXin Li suite_tests.append(test) 225*9c5db199SXin Li 226*9c5db199SXin Li if pretty: 227*9c5db199SXin Li out_as_string = '' 228*9c5db199SXin Li for test in suite_tests: 229*9c5db199SXin Li out_as_string += suite_name + ',' + str(test) + '\n' 230*9c5db199SXin Li return out_as_string 231*9c5db199SXin Li return suite_tests 232*9c5db199SXin Li 233*9c5db199SXin Li def gs_query_link(self, suite_name): 234*9c5db199SXin Li test_names = ','.join([ 235*9c5db199SXin Li test for test in self.list_suite_named(suite_name) 236*9c5db199SXin Li if test != '' 237*9c5db199SXin Li ]) 238*9c5db199SXin Li 239*9c5db199SXin Li query = 'https://dashboards.corp.google.com/' 240*9c5db199SXin Li query += '_86acf8a8_50a5_48e0_829e_fbf1033d3ac6' 241*9c5db199SXin Li query += '?f=test_name:in:' + test_names 242*9c5db199SXin Li query += '&f=create_date_7_day_filter:in:Past%207%20Days' 243*9c5db199SXin Li query += '&f=test_type:in:Tast,Autotest' 244*9c5db199SXin Li 245*9c5db199SXin Li return query 246*9c5db199SXin Li 247*9c5db199SXin Li def graph_suite_named(self, suite_name, dot_graph=None): 248*9c5db199SXin Li suite_tests = self.list_suite_named(suite_name) 249*9c5db199SXin Li nodes_at_rank = 0 250*9c5db199SXin Li 251*9c5db199SXin Li if dot_graph is None: 252*9c5db199SXin Li dot_graph = graphviz.Digraph(comment=suite_name) 253*9c5db199SXin Li 254*9c5db199SXin Li dot_graph.node(suite_name, suite_name) 255*9c5db199SXin Li last_level = suite_name 256*9c5db199SXin Li child_graph = None 257*9c5db199SXin Li 258*9c5db199SXin Li for test_name in suite_tests: 259*9c5db199SXin Li if nodes_at_rank == 0: 260*9c5db199SXin Li child_graph = graphviz.Digraph() 261*9c5db199SXin Li dot_graph.edge(last_level, test_name) 262*9c5db199SXin Li last_level = test_name 263*9c5db199SXin Li 264*9c5db199SXin Li child_graph.node(test_name, test_name) 265*9c5db199SXin Li dot_graph.edge(suite_name, test_name) 266*9c5db199SXin Li 267*9c5db199SXin Li if nodes_at_rank == 6: 268*9c5db199SXin Li dot_graph.subgraph(child_graph) 269*9c5db199SXin Li 270*9c5db199SXin Li nodes_at_rank += 1 271*9c5db199SXin Li nodes_at_rank %= 7 272*9c5db199SXin Li 273*9c5db199SXin Li dot_graph.subgraph(child_graph) 274*9c5db199SXin Li 275*9c5db199SXin Li return dot_graph 276*9c5db199SXin Li 277*9c5db199SXin Li def diff_test_suites(self, suite_a, suite_b): 278*9c5db199SXin Li res = '' 279*9c5db199SXin Li suite_a_set = set(self.list_suite_named(suite_a)) 280*9c5db199SXin Li suite_b_set = set(self.list_suite_named(suite_b)) 281*9c5db199SXin Li res = res + ('Suite B (+)' + str(list(suite_b_set - suite_a_set))) 282*9c5db199SXin Li res = res + '\n' 283*9c5db199SXin Li res = res + ('Suite B (-)' + str(list(suite_a_set - suite_b_set))) 284*9c5db199SXin Li return res 285*9c5db199SXin Li 286*9c5db199SXin Li 287*9c5db199SXin Lidef main(args): 288*9c5db199SXin Li tests = TestManager() 289*9c5db199SXin Li 290*9c5db199SXin Li basepath = os.path.dirname(os.path.abspath(__file__)) 291*9c5db199SXin Li tests.initialize_from_fs([(basepath + '/../test_suites'), 292*9c5db199SXin Li (basepath + '/../server/site_tests'), 293*9c5db199SXin Li (basepath + '/../client/site_tests')]) 294*9c5db199SXin Li tests.process_all_tests() 295*9c5db199SXin Li 296*9c5db199SXin Li if args.csv is not None: 297*9c5db199SXin Li tests.register_csv_logger(args.csv) 298*9c5db199SXin Li if args.dut is not None: 299*9c5db199SXin Li tests.set_dut(args.dut) 300*9c5db199SXin Li if args.find_test is not None: 301*9c5db199SXin Li test = tests.find_test_named(args.find_test) 302*9c5db199SXin Li if test is not None: 303*9c5db199SXin Li tests.log(test.file_path) 304*9c5db199SXin Li else: 305*9c5db199SXin Li tests.log('Queried test not found') 306*9c5db199SXin Li if args.find_suite is not None: 307*9c5db199SXin Li suite = tests.find_suite_named(args.find_suite) 308*9c5db199SXin Li if suite is not None: 309*9c5db199SXin Li tests.log(suite.file_path) 310*9c5db199SXin Li else: 311*9c5db199SXin Li tests.log('Queried suite not found') 312*9c5db199SXin Li if args.list_suite is not None: 313*9c5db199SXin Li tests.log(tests.list_suite_named(args.list_suite, pretty=True)) 314*9c5db199SXin Li if args.list_multiple_suites is not None: 315*9c5db199SXin Li for suite_name in args.list_multiple_suites: 316*9c5db199SXin Li tests.log(tests.list_suite_named(suite_name, pretty=True)) 317*9c5db199SXin Li if args.diff is not None: 318*9c5db199SXin Li tests.log(tests.diff_test_suites(args.diff[0], args.diff[1])) 319*9c5db199SXin Li if args.graph_suite is not None: 320*9c5db199SXin Li graph = tests.graph_suite_named(args.graph_suite) 321*9c5db199SXin Li graph.render('./suite_data/suite_viz.gv', format='png') 322*9c5db199SXin Li if args.gs_dashboard is not None: 323*9c5db199SXin Li link = tests.gs_query_link(args.gs_dashboard) 324*9c5db199SXin Li tests.log(link) 325*9c5db199SXin Li 326*9c5db199SXin Li 327*9c5db199SXin Liif __name__ == '__main__': 328*9c5db199SXin Li # pass in the url for the DUT via ssh 329*9c5db199SXin Li parser = argparse.ArgumentParser() 330*9c5db199SXin Li parser.add_argument('--csv', 331*9c5db199SXin Li help='supply csv file path for logging output') 332*9c5db199SXin Li parser.add_argument( 333*9c5db199SXin Li '--diff', 334*9c5db199SXin Li nargs=2, 335*9c5db199SXin Li help= 336*9c5db199SXin Li 'show diff between two suites. Ex: --diff bvt-tast-cq pvs-tast-cq') 337*9c5db199SXin Li parser.add_argument('--find_test', 338*9c5db199SXin Li help='find control file for test_name') 339*9c5db199SXin Li parser.add_argument('--find_suite', 340*9c5db199SXin Li help='find control file for suite_name') 341*9c5db199SXin Li parser.add_argument( 342*9c5db199SXin Li '--graph_suite', 343*9c5db199SXin Li help= 344*9c5db199SXin Li 'graph test dependencies of suite_name, will output to contrib/suite_data' 345*9c5db199SXin Li ) 346*9c5db199SXin Li parser.add_argument('--list_suite', 347*9c5db199SXin Li help='list units in suite_name') 348*9c5db199SXin Li parser.add_argument( 349*9c5db199SXin Li '--list_multiple_suites', 350*9c5db199SXin Li nargs='*', 351*9c5db199SXin Li help='list units in suite_name_1 suite_name_2 suite_name_n') 352*9c5db199SXin Li parser.add_argument('--dut', 353*9c5db199SXin Li help='ip address and port for tast enumeration') 354*9c5db199SXin Li parser.add_argument( 355*9c5db199SXin Li '--gs_dashboard', 356*9c5db199SXin Li help='generate green stainless dashboard for suite_name') 357*9c5db199SXin Li parsed_args = parser.parse_args() 358*9c5db199SXin Li 359*9c5db199SXin Li main(parsed_args) 360