xref: /aosp_15_r20/external/autotest/contrib/suite_utils.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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