code-coverage: modify intermediate layer algorithm to improve code coverage
- The intermediate layer uses traces from the plugin and DWARF data
from the binaries to match and create code coverage.
- The regulars expresions that group functions within the DWARF
data had been modified in order to capture more functions in the
binaries that correspond to source code.
- A refactoring including more classes were created to encapsulate
how the algorithm was implemented by the code.
Signed-off-by: Saul Romero <saul.romero@arm.com>
diff --git a/coverage-tool/coverage-reporting/intermediate_layer.py b/coverage-tool/coverage-reporting/intermediate_layer.py
index 9ef6f81..5d25a7b 100644
--- a/coverage-tool/coverage-reporting/intermediate_layer.py
+++ b/coverage-tool/coverage-reporting/intermediate_layer.py
@@ -1,6 +1,6 @@
# !/usr/bin/env python
###############################################################################
-# Copyright (c) 2020-2022, ARM Limited and Contributors. All rights reserved.
+# Copyright (c) 2020-2023, ARM Limited and Contributors. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
###############################################################################
@@ -21,10 +21,15 @@
import subprocess
import json
from argparse import RawTextHelpFormatter
-import logging
+import cc_logger
import time
from typing import Dict
from typing import List
+from typing import Generator
+from typing import Union
+from typing import Tuple
+from functools import cached_property
+import logging
__version__ = "7.0"
@@ -178,68 +183,6 @@
return ranges
-def list_of_functions_for_binary(elf_name: str) -> Dict[str, Dict[str, any]]:
- """
- Get an array of the functions in the elf file
-
- :param elf_name: Elf binary file name
- :return: An array of function address start, function address end,
- function dwarf signature (sources) indexed by function name
- """
- _functions = {}
- command = "%s -t %s | awk 'NR>4' | sed /^$/d" % (OBJDUMP, elf_name)
- symbols_output = os_command(command)
- rex = r'([0-9a-fA-F]+) (.{7}) ([^ ]+)[ \t]([0-9a-fA-F]+) (.*)'
- symbols = symbols_output.split('\n')[:-1]
- for sym in symbols:
- try:
- symbol_details = re.findall(rex, sym)
- symbol_details = symbol_details[0]
- if 'F' not in symbol_details[1]:
- continue
- function_name = symbol_details[4]
- # We don't want the .hidden for hidden functions
- if function_name.startswith('.hidden '):
- function_name = function_name[len('.hidden '):]
- if function_name not in _functions:
- _functions[function_name] = {'start': symbol_details[0],
- 'end': symbol_details[3],
- 'sources': False}
- else:
- logger.warning("'{}' duplicated in '{}'".format(
- function_name,
- elf_name))
- except Exception as ex:
- logger.error("@Listing functions at file {}: {}".format(
- elf_name,
- ex))
- return _functions
-
-
-def apply_functions_exclude(elf_config, functions):
- """
- Remove excluded functions from the list of functions
-
- :param elf_config: Config for elf binary file
- :param functions: Array of functions in the binary elf file
- :return: Tuple with included and excluded functions
- """
- if 'exclude_functions' not in elf_config:
- return functions, []
- incl = {}
- excl = {}
- for fname in functions:
- exclude = False
- for rex in elf_config['exclude_functions']:
- if re.match(rex, fname):
- exclude = True
- excl[fname] = functions[fname]
- break
- if not exclude:
- incl[fname] = functions[fname]
- return incl, excl
-
-
def remove_workspace(path, workspace):
"""
Get the relative path to a given workspace
@@ -268,8 +211,11 @@
continue
if cols[1] == "function":
fln[cols[0]] = int(cols[2])
- elif cols[1] == "label" and cols[0] == "func":
- fln[cols[-1]] = int(cols[2])
+ elif cols[1] == "label":
+ if cols[0] == "func":
+ fln[cols[-1]] = int(cols[2])
+ elif cols[0] + ":" == cols[-1]:
+ fln[cols[0]] = int(cols[2])
except BaseException:
logger.warning("Warning: Can't get all function line numbers from %s" %
source_file)
@@ -277,7 +223,6 @@
logger.warning(f"Warning: Unknown error '{ex}' when executing command "
f"'{command}'")
return {}
-
return fln
@@ -299,8 +244,9 @@
if not FUNCTION_LINES_ENABLED:
return 0
if filename not in self.filenames:
- newp = os.path.join(self.workspace, filename)
- self.filenames[filename] = get_function_line_numbers(newp)
+ source_file = os.path.join(self.workspace, filename)
+ # Get all functions with their lines in the source file
+ self.filenames[filename] = get_function_line_numbers(source_file)
return 0 if function_name not in self.filenames[filename] else \
self.filenames[filename][function_name]
@@ -310,52 +256,130 @@
dwarf signature in order to produce logical information to be matched with
traces and produce a code coverage report"""
- def __init__(self, dump: str, function_list: Dict[str, Dict[str, any]],
- _workspace: str, _remove_workspace: bool,
- function_line_numbers: FunctionLineNumbers):
+ def __init__(self, dump: str, _workspace: str, _remove_workspace: bool,
+ local_workspace: str):
"""
Initialisation of the instance to parse binary files.
:param dump: Binary dump (string) containing assembly code and source
code metadata, i.e. source code location and line number.
- :param function_list: Dictionary of functions defined in the binary
- dump.
- :param _workspace: Workspace (folder) where the source files were built.
- :param _remove_workspace: Boolean to indicate if the building of
- source files is local (false) or in a CI (true).
- :param function_line_numbers: Object instance to get a function line
- number within a source code file.
+ :param _workspace: Workspace (folder) where the source files were
+ built from.
+ :param _remove_workspace: Boolean to indicate if the build of
+ source files was local (false) or from a CI (true).
+ :param local_workspace: Path to the local workspace where the source
+ files reside
"""
self.dump = dump
- self.function_list = function_list
+ self.no_source_functions = self.get_no_source_functions()
self.workspace = _workspace
self.remove_workspace = _remove_workspace
- self.function_definition = None
- self.function_line_numbers = function_line_numbers
+ self.local_workspace = local_workspace
+ self.function_line_numbers = FunctionLineNumbers(self.local_workspace)
+
+ def get_no_source_functions(self) -> Dict[int, Dict]:
+ """Find in the dwarf dump all the functions with no source code i.e.:
+ function_name():
+ start_hex_address opcode
+ ....
+ end_hex_address opcode
+
+ :returns: Dictionary of functions indexed by start address function's
+ location
+ """
+ # The functions dict is [start_dec_address]={function name, function
+ # end address in decimal}
+ _functions = {}
+ groups = re.findall(r"(.+?)\(\):\n\s+([a-f0-9]+):."
+ r"+?\n(\s+([a-f0-9]+):.+?\n)*", self.dump)
+ for group in groups:
+ function_name, start_hex_address, _, end_hex_address = group
+ if not end_hex_address:
+ end_hex_address = start_hex_address
+ _functions[int(start_hex_address, 16)] = {'name': function_name,
+ 'end_address': int(
+ end_hex_address, 16)}
+ return _functions
+
+ class SourceCodeBlock(object):
+ """Class used to represent a source code block of information within
+ a function block in a binary dump file.
+ The source code block contains the following components:
+ - Source code file that contains the source code corresponding
+ to the assembly code.
+ - Line number within the source code file corresponding to the source
+ code.
+ - Assembly code block.
+ """
+
+ def __init__(self, source_code_block_dump):
+ """
+ Create an instance of a source code block within a function block.
+
+ :param source_code_block: Tuple of 3 elements that contains the
+ components of a source code block.
+ """
+ self.source_file, self.line_number, self.asm_code \
+ = source_code_block_dump
+
+ @staticmethod
+ def get(dwarf_data: str) -> Generator['BinaryParser.SourceCodeBlock',
+ None, None]:
+ source_block_groups = re.findall(r"(?s)(/[a-zA-Z_0-9][^\n]+?):"
+ r"([0-9]+)(?: [^\n]+)?\n(.+?)"
+ r"\n(?=/[a-zA-Z_0-9][^\n]+?"
+ r":[0-9]+[^\n]+?\n|\n$)",
+ dwarf_data)
+ for source_block_group in source_block_groups:
+ if len(source_block_group) != 3:
+ logger.warning(f"Source code incomplete:"
+ f"{source_block_group}")
+ continue
+ source_block_dump = list(source_block_group)
+ source_block_dump[-1] += "\n\n" # For parsing assembly lines
+ yield BinaryParser.SourceCodeBlock(source_block_dump)
+
+ def __str__(self):
+ return f"'{self.source_file}:{self.line_number}'"
class FunctionBlock(object):
"""Class used to parse and obtain a function block from the
- binary dump file that corresponds to a function declaration within
- the binary assembly code.
+ binary dump file that corresponds to a function declaration in the
+ source code file and a block of assembly code mixed with corresponding
+ source code lines, i.e. dwarf information.
The function block has the following components:
- - Function start address in memory (hexadecimal).
- - Function name.
- - Function code.
+ - Function name at source code.
+ - DWARF data.
+ - Function declaration's line number at source code.
+ This comes from dump blocks like these:
+ 0000000000000230 <_setup>:
+ read_el(): <---- Function name at source code
+ /home/user/aarch64/setup.c:238 <------ Source file and line number
+ 230: d53e1100 mrs x0, scr_el3 <----- Assembly lines belonging to
+ the source code
+ no_setup():
+ /home/user/no_setup.c:618
+ 234: b2760000 orr x0, x0, #0x400
"""
def __init__(self, function_group: List[str]):
"""
Create an instance of a function block within a binary dump.
- :param function_group: List containing the function start
- address, name and code in the function block.
+ :param function_group: List containing the function name and
+ dwarf data of the block.
"""
- self.start, self.name, self.code = function_group
- self.source_file = None
+ self.name, self.dwarf = function_group
+ # Now obtain the function's source file
+ m = re.search(r"(/.+?):([0-9]+)(?: [^\n]+)?\n", self.dwarf)
+ self.source_file = m.groups()[0].strip() \
+ if m and len(m.groups()) == 2 else None
+ # Computed later
self.function_line_number = None
@staticmethod
- def get(dump: str):
+ def get(dump: str) -> Generator['BinaryParser.FunctionBlock', None,
+ None]:
"""
Static method generator to extract a function block from the binary
dump.
@@ -365,43 +389,23 @@
:return: A FunctionBlock object that is a logical representation
of a function declaration within the binary dump.
"""
- function_groups = re.findall(
- r"(?s)([0-9a-fA-F]+) <([a-zA-Z0-9_]+)>:\n(.+?)(?=[A-Fa-f0-9]* "
- r"<[a-zA-Z0-9_]+>:)", dump, re.DOTALL | re.MULTILINE)
+ function_groups = re.findall(r"(?s)([a-zA-Z0-9_]+?)\(\):"
+ r"\n(/.+?:[0-9]+?.+?)\n"
+ r"(?=[a-zA-Z0-9_]+?\(\):\n|\n\n$)",
+ dump)
for group in function_groups:
- if len(group) != 3:
+ if len(group) != 2:
continue
function_group = list(group)
- function_group[-1] += "\n"
+ function_group[-1] += "\n\n" # For parsing source code blocks
yield BinaryParser.FunctionBlock(function_group)
- class SourceCodeBlock(object):
- """Class used to represent a source code block of information within
- a function block in a binary dump file.
- The source code block contains the following components:
- - Optional function name where the source code/assembly code is defined.
- - Source code file that contains the source code corresponding
- to the assembly code.
- - Line number within the source code file corresponding to the source
- code.
- - Assembly code block.
- """
+ @property
+ def values(self):
+ return self.name, self.source_file, self.function_line_number
- def __init__(self, source_code_block):
- """
- Create an instance of a source code block within a function block.
-
- :param source_code_block: Tuple of 4 elements that contains the
- components of a source code block.
- """
- self.function_name, self.source_file, self.line, self.asm_code \
- = source_code_block
-
- def get_assembly_line(self):
- """Getter to return and AssemblyLine instance that corresponds to
- a logical representation of an assembly code line contained
- within a source code block (assembly code block)"""
- return BinaryParser.AssemblyLine.get(self)
+ def __str__(self):
+ return f"'{self.name}:{self.function_line_number}'"
class AssemblyLine(object):
"""Class used to represent an assembly code line within an
@@ -421,67 +425,41 @@
"""
self.hex_line_number, self.opcode = line
self.dec_address = int(self.hex_line_number, 16)
+ self.times_executed = 0
@staticmethod
- def get(source_code_block):
+ def get(asm_code: str) -> Generator['BinaryParser.AssemblyLine',
+ None, None]:
"""
Static method generator to extract an assembly code line from a
assembly code block.
- :param source_code_block: Object that contains the assembly code
- within the source code block.
+ :param asm_code: Lines of assembly code within the dump
:return: AssemblyLine object.
"""
- lines = re.findall(
- r"^[\s]+([a-fA-F0-9]+):\t(.+?)\n",
- source_code_block.asm_code, re.DOTALL | re.MULTILINE)
+ lines = re.findall(r"^\s+([a-fA-F0-9]+):\t(.+?)\n", asm_code,
+ re.DOTALL | re.MULTILINE)
for line in lines:
if len(line) != 2:
+ logger.warning(f"Assembly code incomplete: {line}")
continue
yield BinaryParser.AssemblyLine(line)
- class FunctionDefinition(object):
- """
- Class used to handle a function definition i.e. function name, source
- code filename and line number where is declared.
- """
+ @staticmethod
+ def get_asm_line(source_code_block: 'BinaryParser.SourceCodeBlock',
+ traces_stats) -> \
+ Generator['BinaryParser.AssemblyLine', None, None]:
+ """Generator method to obtain all assembly line codes within a source
+ code line """
+ traces_stats = traces_stats
+ for asm_line in BinaryParser.AssemblyLine.get(
+ source_code_block.asm_code):
+ asm_line.times_executed = traces_stats.get(asm_line.dec_address,
+ [0])[0]
+ yield asm_line
- def __init__(self, function_name):
- """
- Create an instance representing a function definition within a
- function code block.
-
- :param function_name: Initial function name
- """
- self.function_line_number = None
- self.function_name = function_name
- self.source_file: str = None
-
- def update_sources(self, source_files, function_line_numbers):
- """
- Method to update source files dictionary
-
- :param source_files: Dictionary that contains the representation
- of the intermediate layer.
-
- :param function_line_numbers: Object that obtains the start line
- number for a function definition inside it source file.
- :return:Nothing
- """
- source_files.setdefault(self.source_file, {"functions": {},
- "lines": {}})
- if self.function_name not in \
- source_files[self.source_file]["functions"]:
- self.function_line_number = \
- function_line_numbers.get_line_number(
- self.source_file,
- self.function_name)
- source_files[self.source_file]["functions"][
- self.function_name] = {"covered": False,
- "line_number":
- self.function_line_number}
-
- def get_source_code_block(self, function_block: FunctionBlock):
+ def get_source_code_block(self, function_block: FunctionBlock) -> \
+ Generator['BinaryParser.SourceCodeBlock', None, None]:
"""
Generator method to obtain all the source code blocks within a
function block.
@@ -490,61 +468,103 @@
the source code blocks.
:return: A SourceCodeBlock object.
"""
- # When not present the block function name applies
- self.function_definition = BinaryParser.FunctionDefinition(
- function_block.name)
- pattern = r'(?s)(^[a-zA-Z0-9_]+)?(?:\(\):\n)?(^{0}.+?):([0-9]+)[' \
- r'^\n]*\n(.+?)(?={0}.+?:[0-9]+.+\n|^[a-zA-Z0-9_]+\(' \
- r'\):\n)'.format(self.workspace)
- source_code_blocks = re.findall(pattern,
- "{}\n{}/:000".format(
- function_block.code,
- self.workspace),
- re.DOTALL |
- re.MULTILINE)
- for block in source_code_blocks:
- if len(block) != 4:
- continue
- source_code_block = BinaryParser.SourceCodeBlock(block)
- if source_code_block.function_name:
- # Usually in the first iteration function name is not empty
- # and is the function's name block
- self.function_definition.function_name = \
- source_code_block.function_name
- self.function_definition.source_file = source_code_block.source_file
+ for source_code_block in BinaryParser.SourceCodeBlock.get(
+ function_block.dwarf):
if self.remove_workspace:
- self.function_definition.source_file = remove_workspace(
+ source_code_block.source_file = remove_workspace(
source_code_block.source_file, self.workspace)
yield source_code_block
- def get_function_block(self):
+ def get_function_block(self) -> Generator['BinaryParser.FunctionBlock',
+ None, None]:
"""Generator method to obtain all the function blocks contained in
the binary dump file.
"""
for function_block in BinaryParser.FunctionBlock.get(self.dump):
- # Find out if the function block has C source code filename in
- # the function block code
- signature_group = re.findall(
- r"(?s){}\(\):\n(/.+?):[0-9]+.*(?:\r*\n\n|\n$)".format(
- function_block.name), function_block.code,
- re.DOTALL | re.MULTILINE)
- if not signature_group:
- continue # Function does not have dwarf signature (sources)
- if function_block.name not in self.function_list:
- print("Warning:Function '{}' not found in function list!!!".
- format(function_block.name))
- continue # Function not found in function list
- source_code_file = signature_group[0]
- function_block.source_file = source_code_file
+ if function_block.source_file is None:
+ logger.warning(f"Source file not found for function "
+ f"{function_block.name}, will not be covered")
+ continue
if self.remove_workspace:
function_block.source_file = remove_workspace(
- source_code_file, self.workspace)
+ function_block.source_file, self.workspace)
function_block.function_line_number = \
self.function_line_numbers.get_line_number(
function_block.source_file, function_block.name)
yield function_block
+class CoverageHandler(object):
+ """ Class used to handle source files coverage linked with their functions
+ and line code coverage from function blocks obtained from DWARF data and
+ trace code coverage from CC plugin"""
+
+ def __init__(self):
+ self._source_files = {}
+
+ def add_function_coverage(self, function_data:
+ Union[BinaryParser.FunctionBlock,
+ Tuple[str, str, int]]):
+ """ Add a function coverage block and a source file coverage block,
+ if not already created and link them"""
+ # Unpack function data either as an FunctionBlock object property or a
+ # tuple
+ name, source_file, function_line_number = function_data.values if \
+ isinstance(function_data, BinaryParser.FunctionBlock) else \
+ function_data
+
+ # Add source file coverage block it if not already there
+ self._source_files.setdefault(source_file,
+ {"functions": {}, "lines": {}})
+ # Add a function coverage block (if not existent) from a function
+ # block using the function block name as key and link it to the source
+ # file coverage block
+ self._source_files[source_file]["functions"].setdefault(
+ name, {"covered": False, "line_number": function_line_number})
+
+ def add_line_coverage(self, source_code_block:
+ BinaryParser.SourceCodeBlock):
+ """ Add a line coverage block and a source file coverage block,
+ if not already created and link them"""
+ # Add source file coverage block it if not already there
+ self._source_files.setdefault(source_code_block.source_file,
+ {"functions": {}, "lines": {}})
+ # Add a line coverage block (if not existent) from a source block
+ # using the source code line number as key and link it to the source
+ # file coverage block
+ self._source_files[source_code_block.source_file]["lines"].setdefault(
+ source_code_block.line_number, {"covered": False, "elf_index": {}})
+
+ def add_asm_line(self, source_code_block: BinaryParser.SourceCodeBlock,
+ asm_line: BinaryParser.AssemblyLine, elf_index: int):
+ """Add an assembly line from the DWARF data linked to a source code
+ line"""
+ self._source_files[source_code_block.source_file]["lines"][
+ source_code_block.line_number]["elf_index"].setdefault(
+ elf_index, {})
+ self._source_files[source_code_block.source_file]["lines"][
+ source_code_block.line_number]["elf_index"][
+ elf_index].setdefault(asm_line.dec_address,
+ (asm_line.opcode, asm_line.times_executed))
+
+ def set_line_coverage(self, source_code_block:
+ BinaryParser.SourceCodeBlock, value: bool):
+ self._source_files[source_code_block.source_file]["lines"][
+ source_code_block.line_number]["covered"] = value
+
+ def set_function_coverage(self, function_block:
+ Union[BinaryParser.FunctionBlock,
+ Tuple[str, str]], value: bool):
+ name, source_file = (function_block.name, function_block.source_file)\
+ if isinstance(function_block, BinaryParser.FunctionBlock) else \
+ function_block
+ self._source_files[source_file]["functions"][name]["covered"] = value
+
+ @property
+ def source_files(self):
+ return self._source_files
+
+
class IntermediateCodeCoverage(object):
"""Class used to process the trace data along with the dwarf
signature files to produce an intermediate layer in json with
@@ -570,7 +590,7 @@
# 'elf_index'; {elf index}=>{assembly address}=>(opcode,
# times executed),
# 'functions': {function name}=>is covered(boolean)}
- self.source_files_coverage = {}
+ self.coverage = CoverageHandler()
self.functions = []
# Unique set of elf list of files
self.elf_map = {}
@@ -584,7 +604,6 @@
This method writes the intermediate json file output linking
the trace data and c source and assembly code.
"""
- self.source_files_coverage = {}
self.asm_lines = {}
# Initialize for unknown elf files
self.elf_custom = ELF_MAP["custom_offset"]
@@ -594,24 +613,14 @@
for elf in self.elfs:
# Gather information
elf_name = elf['name']
- # Trace data
+ # Obtain trace data
self.traces_stats = load_stats_from_traces(elf['traces'])
- functions_list = list_of_functions_for_binary(elf_name)
- (functions_list, excluded_functions) = apply_functions_exclude(
- elf, functions_list)
# Produce code coverage
- self.process_binary(elf_name, functions_list)
- sources_config = self.config['parameters']['sources']
- # Now check code coverage in the functions with no dwarf signature
- # (sources)
- nf = {f: functions_list[f] for f in
- functions_list if not
- functions_list[f]["sources"]}
- self.process_fn_no_sources(nf)
+ self._process_binary(elf_name)
# Write to the intermediate json file
- data = {"source_files": self.source_files_coverage,
+ data = {"source_files": self.coverage.source_files,
"configuration": {
- "sources": sources_config,
+ "sources": self.config['parameters']['sources'],
"metadata": "" if 'metadata' not in
self.config['parameters'] else
self.config['parameters']['metadata'],
@@ -631,114 +640,119 @@
ELF_MAP["custom_offset"] += 1
return self.elf_map[elf_name]
- def process_binary(self, elf_filename: str, function_list):
+ def _process_binary(self, elf_filename: str) -> BinaryParser:
"""
Process an elf file i.e. match the source code and asm lines against
trace files (coverage).
:param elf_filename: Elf binary file name
- :param function_list: List of functions in the elf file i.e.
- [(address start, address end, function name)]
"""
command = "%s -Sl %s | tee %s" % (OBJDUMP, elf_filename,
elf_filename.replace(".elf", ".dump"))
dump = os_command(command, show_command=True)
- dump += "\n0 <null>:" # For pattern matching the last function
+ # with open(elf_filename.replace(".elf", ".dump"), "r") as f:
+ # dump = f.read()
+ dump += "\n\n" # For pattern matching the last function
+ logger.info(f"Parsing assembly file {elf_filename}")
elf_name = os.path.splitext(os.path.basename(elf_filename))[0]
- function_line_numbers = FunctionLineNumbers(self.local_workspace)
elf_index = self.get_elf_index(elf_name)
- # Pointer to files dictionary
- source_files = self.source_files_coverage
- parser = BinaryParser(dump, function_list, self.workspace,
- self.remove_workspace, function_line_numbers)
+ parser = BinaryParser(dump, self.workspace, self.remove_workspace,
+ self.local_workspace)
+ total_number_functions = 0
+ functions_covered = 0
for function_block in parser.get_function_block():
- function_list[function_block.name]["sources"] = True
- source_files.setdefault(function_block.source_file,
- {"functions": {},
- "lines": {}})
- source_files[function_block.source_file]["functions"][
- function_block.name] = {"covered": False,
- "line_number":
- function_block.function_line_number}
- is_function_block_covered = False
- source_code_block: BinaryParser.SourceCodeBlock
+ total_number_functions += 1
+ # Function contains source code
+ self.coverage.add_function_coverage(function_block)
+ is_function_covered = False
for source_code_block in parser.get_source_code_block(
function_block):
- if parser.function_definition.function_name in function_list:
- function_list[parser.function_definition.function_name][
- "sources"] = True
- parser.function_definition.update_sources(source_files,
- function_line_numbers)
- source_file_ln = \
- source_files[parser.function_definition.source_file][
- "lines"].setdefault(source_code_block.line,
- {"covered": False, "elf_index": {}})
- for asm_block in source_code_block.get_assembly_line():
- times_executed = 0 if \
- asm_block.dec_address not in self.traces_stats else \
- self.traces_stats[asm_block.dec_address][0]
- if times_executed > 0:
- is_function_block_covered = True
- source_file_ln["covered"] = True
- source_files[parser.function_definition.source_file][
- "functions"][
- parser.function_definition.function_name][
- "covered"] = True
- source_file_ln.setdefault("elf_index", {'elf_index': {}})
- if elf_index not in source_file_ln["elf_index"]:
- source_file_ln["elf_index"][elf_index] = {}
- if asm_block.dec_address not in \
- source_file_ln["elf_index"][elf_index]:
- source_file_ln["elf_index"][elf_index][
- asm_block.dec_address] = (
- asm_block.opcode, times_executed)
- source_files[function_block.source_file]["functions"][
- function_block.name]["covered"] |= is_function_block_covered
+ self.coverage.add_line_coverage(source_code_block)
+ is_line_covered = False
+ for asm_line in parser.get_asm_line(source_code_block,
+ self.traces_stats):
+ # Here it is checked the line coverage
+ is_line_covered = asm_line.times_executed > 0 or \
+ is_line_covered
+ self.coverage.add_asm_line(source_code_block, asm_line,
+ elf_index)
+ logger.debug(f"Source file {source_code_block} is "
+ f"{'' if is_line_covered else 'not '}covered")
+ if is_line_covered:
+ self.coverage.set_line_coverage(source_code_block, True)
+ is_function_covered = True
+ logger.debug(f"\tFunction '{function_block.name}' at '"
+ f"{function_block.source_file} is "
+ f"{'' if is_function_covered else 'not '}covered")
+ if is_function_covered:
+ self.coverage.set_function_coverage(function_block, True)
+ functions_covered += 1
+ logger.info(f"Total functions: {total_number_functions}, Functions "
+ f"covered:{functions_covered}")
+ # Now check code coverage in the functions with no dwarf signature
+ self._process_fn_no_sources(parser)
+ return parser
- def process_fn_no_sources(self, function_list):
+ def _process_fn_no_sources(self, parser: BinaryParser):
"""
Checks function coverage for functions with no dwarf signature i.e
sources.
- :param function_list: Dictionary of functions to be checked
+ :param parser: Binary parser that contains objects needed
+ to check function line numbers including the dictionary of functions
+ to be checked i.e [start_dec_address]={'name', 'end_address'}
"""
if not FUNCTION_LINES_ENABLED:
return # No source code at the workspace
- address_seq = sorted(self.traces_stats.keys())
- for function_name in function_list:
- # Just check if the start address is in the trace logs
- covered = function_list[function_name]["start"] in address_seq
- # Find the source file
- files = os_command(("grep --include *.c --include *.s -nrw '{}' {}"
- "| cut -d: -f1").format(function_name,
- self.local_workspace))
- unique_files = set(files.split())
- sources = []
- line_number = 0
+ traces_addresses = sorted(self.traces_stats.keys())
+ traces_address_pointer = 0
+ _functions = parser.no_source_functions
+ functions_addresses = sorted(_functions.keys())
+ address_size = 4
+ for start_address in functions_addresses:
+ function_covered = False
+ function_name = _functions[start_address]['name']
+ # Get all files in the source code where the function is defined
+ source_files = os_command("grep --include '*.c' --include '*.s' "
+ "--include '*.S' -nrw '{}' {}"
+ "| cut -d: -f1".
+ format(function_name,
+ self.local_workspace))
+ unique_files = set(source_files.split())
+ sources_found = []
for source_file in unique_files:
- d = get_function_line_numbers(source_file)
- if function_name in d:
- line_number = d[function_name]
- sources.append(source_file)
- if len(sources) > 1:
- logger.warning("'{}' declared in {} files:{}".format(
- function_name, len(sources),
- ", ".join(sources)))
- elif len(sources) == 1:
- source_file = remove_workspace(sources[0],
- self.local_workspace)
- if source_file not in self.source_files_coverage:
- self.source_files_coverage[source_file] = {"functions": {},
- "lines": {}}
- if function_name not in \
- self.source_files_coverage[source_file]["functions"] \
- or covered:
- self.source_files_coverage[source_file]["functions"][
- function_name] = {"covered": covered,
- "line_number": line_number}
+ line_number = parser.function_line_numbers.get_line_number(
+ source_file, function_name)
+ if line_number > 0:
+ sources_found.append((source_file, line_number))
+ if len(sources_found) == 0:
+ logger.debug(f"'{function_name}' not found in sources")
+ elif len(sources_found) > 1:
+ logger.warning(f"'{function_name}' declared in "
+ f"{len(sources_found)} files")
else:
- logger.warning("Function '{}' not found in sources.".format(
- function_name))
+ source_file_found, function_line_number = sources_found[0]
+ function_source_file = remove_workspace(source_file_found,
+ self.local_workspace)
+ self.coverage.add_function_coverage((function_name,
+ function_source_file,
+ function_line_number))
+ for in_function_address in \
+ range(start_address,
+ _functions[start_address]['end_address']
+ + address_size, address_size):
+ if in_function_address in traces_addresses[
+ traces_address_pointer:]:
+ function_covered = True
+ traces_address_pointer = traces_addresses.index(
+ in_function_address) + 1
+ break
+ logger.info(f"Added non-sources function '{function_name}' "
+ f"with coverage: {function_covered}")
+ if function_covered:
+ self.coverage.set_function_coverage((function_name,
+ function_source_file),
+ function_covered)
json_conf_help = """
@@ -818,6 +832,7 @@
except Exception as ex:
print("Error at opening and processing JSON: {}".format(ex))
return
+ print(json.dumps(config, indent=4))
# Setting toolchain binary tools variables
OBJDUMP = config['parameters']['objdump']
READELF = config['parameters']['readelf']
@@ -841,10 +856,7 @@
if __name__ == '__main__':
- logging.basicConfig(filename='intermediate_layer.log', level=logging.DEBUG,
- format=('%(asctime)s %(levelname)s %(name)s '
- '%(message)s'))
- logger = logging.getLogger(__name__)
+ logger = cc_logger.logger
start_time = time.time()
main()
elapsed_time = time.time() - start_time