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