blob: 7ab5cdc64b343c00035fecd76341dce22f4bb2bd [file] [log] [blame]
#!/usr/bin/env python3
##############################################################################
# Copyright (c) 2020, ARM Limited and Contributors. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
##############################################################################
import os
import sys
import json
import re
import argparse
def function_coverage(function_tuples, info_file):
"""
Parses and get information from intermediate json file to info
file for function coverage
:param function_tuples: List of tuples with function name
and its data as pairs.
:param info_file: Handler to for file writing coverage
"""
total_func = 0
covered_func = 0
function_names = []
function_cov = []
for func_name, func_data in function_tuples:
function_names.append(
'FN:{},{}\n'.format(
func_data["line_number"],
func_name))
total_func += 1
if func_data["covered"]:
covered_func += 1
function_cov.append('FNDA:1,{}\n'.format(func_name))
else:
function_cov.append('FNDA:0,{}\n'.format(func_name))
info_file.write("\n".join(function_names))
info_file.write("\n".join(function_cov))
info_file.write('FNF:{}\n'.format(total_func))
info_file.write('FNH:{}\n'.format(covered_func))
def line_coverage(lines_dict, info_file):
"""
Parses and get information from intermediate json file to info
file for line coverage
:param lines_dict: Dictionary of lines with line number as key
and its data as value
:param info_file: Handler to for file writing coverage
"""
total_lines = 0
covered_lines = 0
for line in lines_dict:
total_lines += 1
if lines_dict[line]['covered']:
covered_lines += 1
info_file.write('DA:' + line + ',1\n')
else:
info_file.write('DA:' + line + ',0\n')
info_file.write('LF:' + str(total_lines) + '\n')
info_file.write('LH:' + str(covered_lines) + '\n')
def sanity_check(branch_line, lines_dict, abs_path_file):
"""
Check if the 'branch_line' line of the C source corresponds to actual
branching instructions in the assembly code. Also, check if that
line is covered. If it's not covered, this branching statement can
be omitted from the report.
Returns False and prints an error message if check is not successful,
True otherwise
:param branch_line: Source code line with the branch instruction
:param lines_dict: Dictionary of lines with line number as key
and its data as value
:param abs_path_file: File name of the source file
"""
if str(branch_line) not in lines_dict:
return False
found_branching = False
for i in lines_dict[str(branch_line)]['elf_index']:
for j in lines_dict[str(branch_line)]['elf_index'][i]:
string = lines_dict[str(branch_line)]['elf_index'][i][j][0]
# these cover all the possible branching instructions
if ('\tb' in string or
'\tcbnz' in string or
'\tcbz' in string or
'\ttbnz' in string or
'\ttbz' in string):
# '\tbl' in string or # already covered by '\tb'
# '\tblr' in string or # already covered by '\tb'
# '\tbr' in string or # already covered by '\tb'
found_branching = True
if not found_branching:
error_log.write(
'\nSomething possibly wrong:\n\tFile ' +
abs_path_file +
', line ' +
str(branch_line) +
'\n\tshould be a branching statement but couldn\'t ' +
'find correspondence in assembly code')
return True
def manage_if_branching(branch_line, lines_dict, info_file, abs_path_file):
"""
Takes care of branch coverage, branch_line is the source code
line in which the 'if' statement is located the function produces
branch coverage info based on C source code and json file content
:param branch_line: Source code line with the 'if' instruction
:param lines_dict: Dictionary of lines with line number as key
and its data as value
:param info_file: Handler to for file writing coverage
:param abs_path_file: File name of the source file
"""
total_branch_local = 0
covered_branch_local = 0
if not sanity_check(branch_line, lines_dict, abs_path_file):
return(total_branch_local, covered_branch_local)
total_branch_local += 2
current_line = branch_line # used to read lines one by one
# check for multiline if-condition and update current_line accordingly
parenthesis_count = 0
while True:
end_of_condition = False
for char in lines[current_line]:
if char == ')':
parenthesis_count -= 1
if parenthesis_count == 0:
end_of_condition = True
elif char == '(':
parenthesis_count += 1
if end_of_condition:
break
current_line += 1
# first branch
# simple case: 'if' statements with no braces
if '{' not in lines[current_line] and '{' not in lines[current_line + 1]:
if (str(current_line + 1) in lines_dict and
lines_dict[str(current_line + 1)]['covered']):
info_file.write('BRDA:' + str(branch_line) + ',0,' + '0,' + '1\n')
covered_branch_local += 1
else:
info_file.write('BRDA:' + str(branch_line) + ',0,' + '0,' + '0\n')
current_line += 1
# more complex case: '{' after the 'if' statement
else:
if '{' in lines[current_line]:
current_line += 1
else:
current_line += 2
# we need to check whether at least one line in the block is covered
found_covered_line = False
# this is a simpler version of a stack used to check when a code block
# ends at the moment, it just checks for '{' and '}', doesn't take into
# account the presence of commented braces
brace_counter = 1
while True:
end_of_block = False
for char in lines[current_line]:
if char == '}':
brace_counter -= 1
if brace_counter == 0:
end_of_block = True
elif char == '{':
brace_counter += 1
if end_of_block:
break
if (str(current_line) in lines_dict and
lines_dict[str(current_line)]['covered']):
found_covered_line = True
current_line += 1
if found_covered_line:
info_file.write('BRDA:' + str(branch_line) + ',0,' + '0,' + '1\n')
covered_branch_local += 1
else:
info_file.write('BRDA:' + str(branch_line) + ',0,' + '0,' + '0\n')
# second branch (if present). If not present, second branch is covered by
# default
current_line -= 1
candidate_else_line = current_line
while 'else' not in lines[current_line] and candidate_else_line + \
2 >= current_line:
current_line += 1
if current_line == len(lines):
break
# no 'else': branch covered by default
if current_line == candidate_else_line + 3:
info_file.write('BRDA:' + str(branch_line) + ',0,' + '1,' + '1\n')
covered_branch_local += 1
return(total_branch_local, covered_branch_local)
# 'else' found: check if opening braces are present
if '{' not in lines[current_line - 1] and '{' not in lines[current_line]:
if str(current_line + 1) in lines_dict:
if lines_dict[str(current_line + 1)]['covered']:
info_file.write(
'BRDA:' +
str(branch_line) +
',0,' +
'1,' +
'1\n')
covered_branch_local += 1
else:
info_file.write(
'BRDA:' +
str(branch_line) +
',0,' +
'1,' +
'0\n')
else:
info_file.write('BRDA:' + str(branch_line) + ',0,' + '1,' + '0\n')
else:
if '{' in lines[current_line]:
current_line += 1
else:
current_line += 2
found_covered_line = False
while '}' not in lines[current_line]:
if (str(current_line) in lines_dict and
lines_dict[str(current_line)]['covered']):
found_covered_line = True
break
current_line += 1
if found_covered_line:
info_file.write('BRDA:' + str(branch_line) + ',0,' + '1,' + '1\n')
covered_branch_local += 1
else:
info_file.write('BRDA:' + str(branch_line) + ',0,' + '1,' + '0\n')
return(total_branch_local, covered_branch_local)
def manage_switch_branching(switch_line, lines_dict, info_file, abs_path_file):
"""
Takes care of branch coverage, branch_line is the source code
line in which the 'switch' statement is located the function produces
branch coverage info based on C source code and json file content
:param switch_line: Source code line with the 'switch' instruction
:param lines_dict: Dictionary of lines with line number as key
and its data as value
:param info_file: Handler to for file writing coverage
:param abs_path_file: File name of the source file
"""
total_branch_local = 0
covered_branch_local = 0
if not sanity_check(switch_line, lines_dict, abs_path_file):
return(total_branch_local, covered_branch_local)
current_line = switch_line # used to read lines one by one
branch_counter = 0 # used to count the number of switch branches
brace_counter = 0
# parse the switch-case line by line, checking if every 'case' is covered
# the switch-case ends with a '}'
while True:
if '{' in lines[current_line]:
brace_counter += 1
if '}' in lines[current_line]:
brace_counter -= 1
if brace_counter == 0:
return(total_branch_local, covered_branch_local)
if 'case' in lines[current_line] or 'default' in lines[current_line]:
covered = False
total_branch_local += 1
inner_brace = 0
current_line += 1
while (('case' not in lines[current_line]
and 'default' not in lines[current_line]) or
inner_brace > 0):
if (str(current_line) in lines_dict and
lines_dict[str(current_line)]['covered']):
covered = True
if '{' in lines[current_line]:
inner_brace += 1
brace_counter += 1
if '}' in lines[current_line]:
inner_brace -= 1
brace_counter -= 1
if brace_counter == 0:
break
current_line += 1
if covered:
info_file.write(
'BRDA:' +
str(switch_line) +
',0,' +
str(branch_counter) +
',1\n')
covered_branch_local += 1
else:
info_file.write(
'BRDA:' +
str(switch_line) +
',0,' +
str(branch_counter) +
',0\n')
if brace_counter == 0:
return(total_branch_local, covered_branch_local)
branch_counter += 1
else:
current_line += 1
return(total_branch_local, covered_branch_local)
def branch_coverage(abs_path_file, info_file, lines_dict):
"""
Produces branch coverage information, using the functions
'manage_if_branching' and 'manage_switch_branching'
:param abs_path_file: File name of the source file
:param info_file: Handler to for file writing coverage
:param lines_dict: Dictionary of lines with line number as key
and its data as value
"""
total_branch = 0
covered_branch = 0
# branch coverage: if statements
branching_lines = []
# regex: find all the lines starting with 'if' or 'else if'
# (possibly preceded by whitespaces/tabs)
pattern = re.compile(r"^\s+if|^\s+} else if|^\s+else if")
for i, line in enumerate(open(abs_path_file)):
for match in re.finditer(pattern, line):
branching_lines.append(i + 1)
while branching_lines:
t = manage_if_branching(branching_lines.pop(0), lines_dict,
info_file, abs_path_file)
total_branch += t[0]
covered_branch += t[1]
# branch coverage: switch statements
switch_lines = []
# regex: find all the lines starting with 'switch'
# (possibly preceded by whitespaces/tabs)
pattern = re.compile(r"^\s+switch")
for i, line in enumerate(open(abs_path_file)):
for match in re.finditer(pattern, line):
switch_lines.append(i + 1)
while switch_lines:
t = manage_switch_branching(switch_lines.pop(0), lines_dict,
info_file, abs_path_file)
total_branch += t[0]
covered_branch += t[1]
info_file.write('BRF:' + str(total_branch) + '\n')
info_file.write('BRH:' + str(covered_branch) + '\n')
parser = argparse.ArgumentParser(
description="Script to convert intermediate json file to LCOV info file")
parser.add_argument('--workspace', metavar='PATH',
help='Folder with source files structure',
required=True)
parser.add_argument('--json', metavar='PATH',
help='Intermediate json file name',
required=True)
parser.add_argument('--info', metavar='PATH',
help='Output info file name',
default="coverage.info")
args = parser.parse_args()
with open(args.json) as json_file:
json_data = json.load(json_file)
info_file = open(args.info, "w+")
error_log = open("error_log.txt", "w+")
file_list = json_data['source_files'].keys()
for relative_path in file_list:
abs_path_file = os.path.join(args.workspace, relative_path)
if not os.path.exists(abs_path_file):
continue
source = open(abs_path_file)
lines = source.readlines()
info_file.write('TN:\n')
info_file.write('SF:' + os.path.abspath(abs_path_file) + '\n')
lines = [-1] + lines # shifting the lines indexes to the right
function_coverage(
json_data['source_files'][relative_path]['functions'].items(),
info_file)
branch_coverage(abs_path_file, info_file,
json_data['source_files'][relative_path]['lines'])
line_coverage(json_data['source_files'][relative_path]['lines'],
info_file)
info_file.write('end_of_record\n\n')
source.close()
json_file.close()
info_file.close()
error_log.close()