1# SPDX-License-Identifier: Apache-2.0
2# Copyright (c) 2024 Intel Corporation
3
4import yaml
5import ijson
6import json
7import re
8import argparse
9import xlsxwriter
10class Json_report:
11
12    json_object = {
13        "components":[]
14    }
15
16    simulators = [
17        'unit_testing',
18        'native',
19        'qemu',
20        'mps2/an385'
21    ]
22
23    report_json = {}
24
25    def __init__(self):
26        args = parse_args()
27        self.parse_testplan(args.testplan)
28        self.maintainers_file = self.get_maintainers_file( args.maintainers)
29        self.report_json = self.generate_json_report( args.coverage)
30
31        if args.format == "json":
32            self.save_json_report( args.output, self.report_json)
33        elif args.format == "xlsx":
34            self.generate_xlsx_report(self.report_json, args.output)
35        elif args.format == "all":
36            self.save_json_report( args.output, self.report_json)
37            self.generate_xlsx_report(self.report_json, args.output)
38        else:
39            print("Format incorrect")
40
41    def get_maintainers_file(self, maintainers):
42        maintainers_file = ""
43        with open(maintainers, 'r') as file:
44            maintainers_file = yaml.safe_load(file)
45            file.close()
46        return maintainers_file
47
48
49    def parse_testplan(self, testplan_path):
50        with open(testplan_path, 'r') as file:
51            parser = ijson.items(file, 'testsuites')
52            for element in parser:
53                for testsuite in element:
54                    for testcase in testsuite['testcases']:
55                        if testcase['status'] == 'None':
56                            testcase_name = testcase['identifier']
57                            component_name = testcase_name[:testcase_name.find('.')]
58                            component = {
59                                "name": component_name,
60                                "sub_components":[],
61                                "files":[]
62                            }
63                            features = self.json_object['components']
64                            known_component_flag = False
65                            for item in features:
66                                if component_name == item['name']:
67                                    component = item
68                                    known_component_flag = True
69                                    break
70                            sub_component_name = testcase_name[testcase_name.find('.'):]
71                            sub_component_name = sub_component_name[1:]
72                            if sub_component_name.find(".") > 0:
73                                sub_component_name = sub_component_name[:sub_component_name.find(".")]
74                            if known_component_flag is False:
75
76                                sub_component = {
77                                    "name":sub_component_name,
78                                    "test_suites":[]
79                                }
80                                test_suite = {
81                                    "name":testsuite['name'],
82                                    "path":testsuite['path'],
83                                    "platforms":[],
84                                    "runnable": testsuite['runnable'],
85                                    "status":"",
86                                    "test_cases":[]
87                                }
88                                test_case = {
89                                    "name":testcase_name
90                                }
91                                if any(platform in testsuite['platform'] for platform in self.simulators):
92                                    if test_suite['status'] == "":
93                                        test_suite['status'] = 'sim_only'
94
95                                    if test_suite['status'] == 'hw_only':
96                                        test_suite['status'] = 'mixed'
97                                else:
98                                    if test_suite['status'] == "":
99                                        test_suite['status'] = 'hw_only'
100
101                                    if test_suite['status'] == 'sim_only':
102                                        test_suite['status'] = 'mixed'
103                                test_suite['test_cases'].append(test_case)
104                                test_suite['platforms'].append(testsuite['platform'])
105                                sub_component["test_suites"].append(test_suite)
106                                component['sub_components'].append(sub_component)
107                                self.json_object['components'].append(component)
108                            else:
109                                sub_component = {}
110                                sub_components = component['sub_components']
111                                known_sub_component_flag = False
112                                for i_sub_component in sub_components:
113                                    if sub_component_name == i_sub_component['name']:
114                                        sub_component = i_sub_component
115                                        known_sub_component_flag = True
116                                        break
117                                if known_sub_component_flag is False:
118                                    sub_component = {
119                                        "name":sub_component_name,
120                                        "test_suites":[]
121                                    }
122                                    test_suite = {
123                                        "name":testsuite['name'],
124                                        "path":testsuite['path'],
125                                        "platforms":[],
126                                        "runnable": testsuite['runnable'],
127                                        "status":"",
128                                        "test_cases":[]
129                                    }
130                                    test_case = {
131                                        "name": testcase_name
132                                    }
133                                    if any(platform in testsuite['platform'] for platform in self.simulators):
134                                        if test_suite['status'] == "":
135                                            test_suite['status'] = 'sim_only'
136
137                                        if test_suite['status'] == 'hw_only':
138                                            test_suite['status'] = 'mixed'
139                                    else:
140                                        if test_suite['status'] == "":
141                                            test_suite['status'] = 'hw_only'
142
143                                        if test_suite['status'] == 'sim_only':
144                                            test_suite['status'] = 'mixed'
145                                    test_suite['test_cases'].append(test_case)
146                                    test_suite['platforms'].append(testsuite['platform'])
147                                    sub_component["test_suites"].append(test_suite)
148                                    component['sub_components'].append(sub_component)
149                                else:
150                                    test_suite = {}
151                                    test_suites = sub_component['test_suites']
152                                    known_testsuite_flag = False
153                                    for i_testsuite in test_suites:
154                                        if testsuite['name'] == i_testsuite['name']:
155                                            test_suite = i_testsuite
156                                            known_testsuite_flag = True
157                                            break
158                                    if known_testsuite_flag is False:
159                                        test_suite = {
160                                            "name":testsuite['name'],
161                                            "path":testsuite['path'],
162                                            "platforms":[],
163                                            "runnable": testsuite['runnable'],
164                                            "status":"",
165                                            "test_cases":[]
166                                        }
167                                        test_case  = {
168                                            "name": testcase_name
169                                        }
170                                        if any(platform in testsuite['platform'] for platform in self.simulators):
171                                            if test_suite['status'] == "":
172                                                test_suite['status'] = 'sim_only'
173
174                                            if test_suite['status'] == 'hw_only':
175                                                test_suite['status'] = 'mixed'
176                                        else:
177                                            if test_suite['status'] == "":
178                                                test_suite['status'] = 'hw_only'
179
180                                            if test_suite['status'] == 'sim_only':
181                                                test_suite['status'] = 'mixed'
182                                        test_suite['test_cases'].append(test_case)
183                                        test_suite['platforms'].append(testsuite['platform'])
184                                        sub_component["test_suites"].append(test_suite)
185                                    else:
186                                        if any(platform in testsuite['platform'] for platform in self.simulators):
187                                            if test_suite['status'] == "":
188                                                test_suite['status'] = 'sim_only'
189
190                                            if test_suite['status'] == 'hw_only':
191                                                test_suite['status'] = 'mixed'
192                                        else:
193                                            if test_suite['status'] == "":
194                                                test_suite['status'] = 'hw_only'
195
196                                            if test_suite['status'] == 'sim_only':
197                                                test_suite['status'] = 'mixed'
198                                        test_case = {}
199                                        test_cases = test_suite['test_cases']
200                                        known_testcase_flag = False
201                                        for i_testcase in test_cases:
202                                            if testcase_name == i_testcase['name']:
203                                                test_case = i_testcase
204                                                known_testcase_flag = True
205                                                break
206                                        if known_testcase_flag is False:
207                                            test_case = {
208                                                "name":testcase_name
209                                            }
210                                            test_suite['test_cases'].append(test_case)
211            file.close()
212
213    def get_files_from_maintainers_file(self, component_name):
214        files_path = []
215        for item in self.maintainers_file:
216            _found_flag = False
217            try:
218                tests = self.maintainers_file[item].get('tests', [])
219                for i_test in tests:
220                    if component_name in i_test:
221                        _found_flag = True
222
223                if _found_flag is True:
224                    for path in self.maintainers_file[item]['files']:
225                        path = path.replace('*','.*')
226                        files_path.append(path)
227            except TypeError:
228                print("ERROR: Fail while parsing MAINTAINERS file at %s", component_name)
229        return files_path
230
231    def generate_json_report(self, coverage):
232        output_json = {
233            "components":[]
234        }
235
236        with open(coverage, 'r') as file:
237            parser = ijson.items(file, 'files')
238            for element in parser:
239
240                for i_json_component in self.json_object['components']:
241                    json_component = {}
242                    json_component["name"]=i_json_component["name"]
243                    json_component["sub_components"] = i_json_component["sub_components"]
244                    json_component["Comment"] = ""
245                    files_path = []
246                    files_path = self.get_files_from_maintainers_file(i_json_component["name"])
247                    json_files = []
248                    if len(files_path) != 0:
249                        for i_file in files_path:
250                            for i_covered_file in element:
251                                x = re.search(('.*'+i_file+'.*'), i_covered_file['file'])
252                                if x:
253                                    file_name =  i_covered_file['file'][i_covered_file['file'].rfind('/')+1:]
254                                    file_path = i_covered_file['file']
255                                    file_coverage, file_lines, file_hit = self._calculate_coverage_of_file(i_covered_file)
256                                    json_file = {
257                                            "Name":file_name,
258                                            "Path":file_path,
259                                            "Lines": file_lines,
260                                            "Hit":file_hit,
261                                            "Coverage": file_coverage,
262                                            "Covered_Functions": [],
263                                            "Uncovered_Functions": []
264                                        }
265                                    for i_fun in i_covered_file['functions']:
266                                        if i_fun['execution_count'] != 0:
267                                            json_covered_funciton ={
268                                                "Name":i_fun['name']
269                                            }
270                                            json_file['Covered_Functions'].append(json_covered_funciton)
271                                    for i_fun in i_covered_file['functions']:
272                                        if i_fun['execution_count'] == 0:
273                                            json_uncovered_funciton ={
274                                                "Name":i_fun['name']
275                                            }
276                                            json_file['Uncovered_Functions'].append(json_uncovered_funciton)
277                                    comp_exists = [x for x in json_files if x['Path'] == json_file['Path']]
278                                    if not comp_exists:
279                                        json_files.append(json_file)
280                        json_component['files']=json_files
281                        output_json['components'].append(json_component)
282                    else:
283                        json_component["files"] = []
284                        json_component["Comment"] = "Missed in maintainers.yml file."
285                        output_json['components'].append(json_component)
286
287        return output_json
288
289    def _calculate_coverage_of_file(self, file):
290        tracked_lines = len(file['lines'])
291        covered_lines = 0
292        for line in file['lines']:
293            if line['count'] != 0:
294                covered_lines += 1
295        return ((covered_lines/tracked_lines)*100), tracked_lines, covered_lines
296
297    def save_json_report(self, output_path, json_object):
298        json_object = json.dumps(json_object, indent=4)
299        with open(output_path+'.json', "w") as outfile:
300            outfile.write(json_object)
301
302    def _find_char(self, path, str, n):
303        sep = path.split(str, n)
304        if len(sep) <= n:
305            return -1
306        return len(path) - len(sep[-1]) - len(str)
307
308    def _component_calculate_stats(self, json_component):
309        testsuites_count = 0
310        runnable_count = 0
311        build_only_count = 0
312        sim_only_count = 0
313        hw_only_count = 0
314        mixed_count = 0
315        for i_sub_component in json_component['sub_components']:
316            for i_testsuit in i_sub_component['test_suites']:
317                testsuites_count += 1
318                if i_testsuit['runnable'] is True:
319                    runnable_count += 1
320                else:
321                    build_only_count += 1
322
323                if i_testsuit['status'] == "hw_only":
324                    hw_only_count += 1
325                elif i_testsuit['status'] == "sim_only":
326                    sim_only_count += 1
327                else:
328                    mixed_count += 1
329        return testsuites_count, runnable_count, build_only_count, sim_only_count, hw_only_count, mixed_count
330
331    def _xlsx_generate_summary_page(self, workbook, json_report):
332        # formats
333        header_format = workbook.add_format(
334            {
335                "bold": True,
336                "fg_color":  "#538DD5",
337                "color":"white"
338            }
339        )
340        cell_format = workbook.add_format(
341            {
342                'valign': 'vcenter'
343            }
344        )
345
346        #generate summary page
347        worksheet = workbook.add_worksheet('Summary')
348        row = 0
349        col = 0
350        worksheet.write(row,col,"Components",header_format)
351        worksheet.write(row,col+1,"TestSuites",header_format)
352        worksheet.write(row,col+2,"Runnable",header_format)
353        worksheet.write(row,col+3,"Build only",header_format)
354        worksheet.write(row,col+4,"Simulators only",header_format)
355        worksheet.write(row,col+5,"Hardware only",header_format)
356        worksheet.write(row,col+6,"Mixed",header_format)
357        worksheet.write(row,col+7,"Coverage [%]",header_format)
358        worksheet.write(row,col+8,"Total Functions",header_format)
359        worksheet.write(row,col+9,"Uncovered Functions",header_format)
360        worksheet.write(row,col+10,"Comment",header_format)
361        row = 1
362        col = 0
363        for item in json_report['components']:
364            worksheet.write(row, col, item['name'],cell_format)
365            testsuites,runnable,build_only,sim_only,hw_only, mixed= self._component_calculate_stats(item)
366            worksheet.write(row,col+1,testsuites,cell_format)
367            worksheet.write(row,col+2,runnable,cell_format)
368            worksheet.write(row,col+3,build_only,cell_format)
369            worksheet.write(row,col+4,sim_only,cell_format)
370            worksheet.write(row,col+5,hw_only,cell_format)
371            worksheet.write(row,col+6,mixed,cell_format)
372            lines = 0
373            hit = 0
374            coverage = 0.0
375            total_funs = 0
376            uncovered_funs = 0
377            for i_file in item['files']:
378                lines += i_file['Lines']
379                hit += i_file['Hit']
380                total_funs += (len(i_file['Covered_Functions'])+len(i_file['Uncovered_Functions']))
381                uncovered_funs += len(i_file['Uncovered_Functions'])
382
383            if lines != 0:
384                coverage = (hit/lines)*100
385
386            worksheet.write_number(row,col+7,coverage,workbook.add_format({'num_format':'#,##0.00'}))
387            worksheet.write_number(row,col+8,total_funs)
388            worksheet.write_number(row,col+9,uncovered_funs)
389            worksheet.write(row,col+10,item["Comment"],cell_format)
390            row += 1
391            col = 0
392        worksheet.conditional_format(1,col+7,row,col+7, {'type':     'data_bar',
393                                                            'min_value':  0,
394                                                            'max_value':  100,
395                                                            'bar_color': '#3fd927',
396                                                            'bar_solid': True,
397                                                                })
398        worksheet.autofit()
399        worksheet.set_default_row(15)
400
401    def generate_xlsx_report(self, json_report, output):
402        self.report_book = xlsxwriter.Workbook(output+".xlsx")
403        header_format = self.report_book.add_format(
404            {
405                "bold": True,
406                "fg_color":  "#538DD5",
407                "color":"white"
408            }
409        )
410
411        # Create a format to use in the merged range.
412        merge_format = self.report_book.add_format(
413            {
414                "bold": 1,
415                "align": "center",
416                "valign": "vcenter",
417                "fg_color":  "#538DD5",
418                "color":"white"
419            }
420        )
421        cell_format = self.report_book.add_format(
422            {
423                'valign': 'vcenter'
424            }
425        )
426
427        self._xlsx_generate_summary_page(self.report_book, self.report_json)
428        row = 0
429        col = 0
430        for item in json_report['components']:
431            worksheet = self.report_book.add_worksheet(item['name'])
432            row = 0
433            col = 0
434            worksheet.write(row,col,"File Name",header_format)
435            worksheet.write(row,col+1,"File Path",header_format)
436            worksheet.write(row,col+2,"Coverage [%]",header_format)
437            worksheet.write(row,col+3,"Lines",header_format)
438            worksheet.write(row,col+4,"Hits",header_format)
439            worksheet.write(row,col+5,"Diff",header_format)
440            row += 1
441            col = 0
442            for i_file in item['files']:
443                worksheet.write(row,col,i_file['Path'][i_file['Path'].rfind('/')+1:],cell_format)
444                worksheet.write(row,col+1,i_file["Path"][(self._find_char(i_file["Path"],'/',3)+1):],cell_format)
445                worksheet.write_number(row,col+2,i_file["Coverage"],self.report_book.add_format({'num_format':'#,##0.00'}))
446                worksheet.write(row,col+3,i_file["Lines"],cell_format)
447                worksheet.write(row,col+4,i_file["Hit"],cell_format)
448                worksheet.write(row,col+5,i_file["Lines"]-i_file["Hit"],cell_format)
449                row += 1
450                col = 0
451            row += 1
452            col = 0
453            worksheet.conditional_format(1,col+2,row,col+2, {'type':     'data_bar',
454                                                            'min_value':  0,
455                                                            'max_value':  100,
456                                                            'bar_color': '#3fd927',
457                                                            'bar_solid': True,
458                                                                })
459            worksheet.merge_range(row,col,row,col+2, "Uncovered Functions", merge_format)
460            row += 1
461            worksheet.write(row,col,'Function Name',header_format)
462            worksheet.write(row,col+1,'Implementation File',header_format)
463            worksheet.write(row,col+2,'Comment',header_format)
464            row += 1
465            col = 0
466            for i_file in item['files']:
467                for i_uncov_fun in i_file['Uncovered_Functions']:
468                    worksheet.write(row,col,i_uncov_fun["Name"],cell_format)
469                    worksheet.write(row,col+1,i_file["Path"][self._find_char(i_file["Path"],'/',3)+1:],cell_format)
470                    row += 1
471                    col = 0
472            row += 1
473            col = 0
474            worksheet.write(row,col,"Components",header_format)
475            worksheet.write(row,col+1,"Sub-Components",header_format)
476            worksheet.write(row,col+2,"TestSuites",header_format)
477            worksheet.write(row,col+3,"Runnable",header_format)
478            worksheet.write(row,col+4,"Build only",header_format)
479            worksheet.write(row,col+5,"Simulation only",header_format)
480            worksheet.write(row,col+6,"Hardware only",header_format)
481            worksheet.write(row,col+7,"Mixed",header_format)
482            row += 1
483            col = 0
484            worksheet.write(row,col,item['name'],cell_format)
485            for i_sub_component in item['sub_components']:
486                testsuites_count = 0
487                runnable_count = 0
488                build_only_count = 0
489                sim_only_count = 0
490                hw_only_count = 0
491                mixed_count = 0
492                worksheet.write(row,col+1,i_sub_component['name'],cell_format)
493                for i_testsuit in i_sub_component['test_suites']:
494                    testsuites_count += 1
495                    if i_testsuit['runnable'] is True:
496                        runnable_count += 1
497                    else:
498                        build_only_count += 1
499
500                    if i_testsuit['status'] == "hw_only":
501                        hw_only_count += 1
502                    elif i_testsuit['status'] == "sim_only":
503                        sim_only_count += 1
504                    else:
505                        mixed_count += 1
506                worksheet.write(row,col+2,testsuites_count,cell_format)
507                worksheet.write(row,col+3,runnable_count,cell_format)
508                worksheet.write(row,col+4,build_only_count,cell_format)
509                worksheet.write(row,col+5,sim_only_count,cell_format)
510                worksheet.write(row,col+6,hw_only_count,cell_format)
511                worksheet.write(row,col+7,mixed_count,cell_format)
512                row += 1
513                col = 0
514
515            worksheet.autofit()
516            worksheet.set_default_row(15)
517        self.report_book.close()
518
519def parse_args():
520    parser = argparse.ArgumentParser(allow_abbrev=False)
521    parser.add_argument('-m','--maintainers', help='Path to maintainers.yml [Required]', required=True)
522    parser.add_argument('-t','--testplan', help='Path to testplan [Required]', required=True)
523    parser.add_argument('-c','--coverage', help='Path to components file [Required]', required=True)
524    parser.add_argument('-o','--output', help='Report name [Required]', required=True)
525    parser.add_argument('-f','--format', help='Output format (json, xlsx, all) [Required]', required=True)
526
527    args = parser.parse_args()
528    return args
529
530if __name__ == '__main__':
531    Json_report()
532