diff --git a/coverage-tool/coverage-reporting/cc_logger.py b/coverage-tool/coverage-reporting/cc_logger.py new file mode 100644 index 0000000000000000000000000000000000000000..67bf984e2701fe0b323e9659e99590df221222d4 --- /dev/null +++ b/coverage-tool/coverage-reporting/cc_logger.py @@ -0,0 +1,28 @@ +import logging +import time + +time_file = time.strftime("%Y%m%d-%H%M%S") +# Create a global logger +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# Create handlers +c_handler = logging.StreamHandler() +f_handler = logging.FileHandler(f'cc_logger.log') +c_handler.setLevel(logging.DEBUG) +f_handler.setLevel(logging.DEBUG) + +# Create formatters and add it to handlers +c_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(' + 'message)s') +f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(' + 'message)s') +c_handler.setFormatter(c_format) +f_handler.setFormatter(f_format) + +# Add handlers to the logger +logger.addHandler(c_handler) +logger.addHandler(f_handler) + + + diff --git a/coverage-tool/coverage-reporting/intermediate_layer.py b/coverage-tool/coverage-reporting/intermediate_layer.py index 9ef6f8109d48307aadd1643e30d05086e92320fb..5d25a7b189ed911fe9f64232f695d63086c11a84 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 argparse 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 @@ def get_executable_ranges_for_binary(elf_name): 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 @@ def get_function_line_numbers(source_file: str) -> Dict[str, int]: 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 @@ def get_function_line_numbers(source_file: str) -> Dict[str, int]: logger.warning(f"Warning: Unknown error '{ex}' when executing command " f"'{command}'") return {} - return fln @@ -299,8 +244,9 @@ class FunctionLineNumbers(object): 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 @@ class BinaryParser(object): 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 @@ class BinaryParser(object): :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 @@ class BinaryParser(object): """ 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. - """ - - 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): + @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 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 @@ class BinaryParser(object): 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 @@ class IntermediateCodeCoverage(object): # '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 @@ class IntermediateCodeCoverage(object): 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 @@ class IntermediateCodeCoverage(object): 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 @@ class IntermediateCodeCoverage(object): 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 :" # 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 - - def process_fn_no_sources(self, function_list): + 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, 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 @@ def main(): 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 @@ def main(): 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 diff --git a/coverage-tool/coverage-reporting/merge.sh b/coverage-tool/coverage-reporting/merge.sh index e0ec69cdcc05b88c262879f8434c5ea6360d0872..635b1f20a4c6f566858a95616bd7a0807f88e80b 100755 --- a/coverage-tool/coverage-reporting/merge.sh +++ b/coverage-tool/coverage-reporting/merge.sh @@ -12,8 +12,7 @@ # DESCRIPTION: Wrapper to merge intermediate json files and LCOV trace .info # files. #============================================================================== - -set +x +set -x ################################################################# # Function to manipulate json objects. # The json object properties can be accessed through "." separated @@ -159,8 +158,7 @@ get_file() { if [ "$_type" = '"http"' ];then fname="$where.$extension" # Same filename as folder rm $where/$fname &>/dev/null || true - wget -o error.log $_origin -O $where/$fname || ( - cat error.log && exit -1) + wget -nv $_origin -O $where/$fname || return -1 cloned_file="$(get_abs_path $where/$fname)" elif [ "$_type" = '"bundle"' ];then # Check file exists at origin, i.e. was unbundled before @@ -171,14 +169,14 @@ get_file() { elif [ "$_type" = '"file"' ];then if [[ "$_origin" = http* ]]; then echo "$_origin looks like 'http' rather than 'file' please check..." - exit -1 + return -1 fi fname="$where.$extension" # Same filename as folder cp -f $_origin $where/$fname cloned_file="$(get_abs_path $where/$fname)" else echo "Error unsupported file type:$_type.... Aborting." - exit -1 + return -1 fi if [ "$_compression" = "tar.xz" ];then cd $where @@ -188,6 +186,7 @@ get_file() { cd - fi eval "${var_name}=${cloned_file}" + return 0 } ##################################################################### @@ -207,6 +206,8 @@ get_info_json_files() { local json_string="$(cat $merge_configuration_file)" local config_json_file="" local info_file="" + + #printf "\tReading from JSON data:\n\t\t%s" $json_string # Get files array local nf=$(get_json_object "$json_string" "-files") # Init target folder @@ -218,8 +219,10 @@ get_info_json_files() { pushd $input_folder > /dev/null _file=$(get_json_object "$json_string" "files.$f") # The name of the folder is the 'id' value - folder=$(get_json_object "$_file" "*id") - echo "Getting files from project '$folder' into '$input_folder'..." + id=$(get_json_object "$_file" "*id") + tf_config=$(get_json_object "$_file" "*tf-configuration" "N/A") + folder=$id + printf "Getting files from configuration '$tf_config', build '$folder' into '$input_folder'...\n" mkdir -p $folder bundles=$(get_json_object "$_file" "bundles" None) if [ "$bundles" != "None" ];then @@ -229,10 +232,11 @@ get_info_json_files() { get_file "$(get_json_object "$bundles" "$n")" $folder done fi - # Download/copy files and save their locations - get_file "$(get_json_object "$_file" "config")" $folder config_json_file - get_file "$(get_json_object "$_file" "info")" $folder info_file - info_files+=($info_file) + # Download/copy files and save their locations only if all are found + get_file "$(get_json_object "$_file" "config")" $folder config_json_file && \ + get_file "$(get_json_object "$_file" "info")" $folder info_file && \ + info_files+=($info_file) && json_files+=($config_json_file) && \ + list_of_merged_builds+=($id) popd > /dev/null done } @@ -244,15 +248,17 @@ get_info_json_files() { # merged_json_file: Location and name for merged json scm sources # LOCAL_WORKSPACE: Local workspace folder with the source code files # generate_local: Flag to generate local lcov reports +# info_files: Array of locations and names of info files +# json_files: Array of locations and names of json files # Arguments: # 1: Location where reside json and info files # Outputs: # Merged coverage file -# Merged json file +# Merged json configuration file ################################################################ merge_files() { - local input_folder="${1:-$LCOV_FOLDER}" -# Merge info and json files +# Merge info and json configuration files + printf "\tFound report files from %d code coverage folders to be merged...\n" ${#info_files[@]} local lc=" " if [ -n "$LOCAL_WORKSPACE" ];then # Translation from info workspaces into local workspace @@ -265,8 +271,8 @@ merge_files() { # Getting the path of the merge.py must reside at the same # path as the merge.sh python3 ${DIR}/merge.py \ - $(find $input_folder -name "*.info" -exec echo "-a {}" \;) \ - $(find $input_folder -name "*.json" -exec echo "-j {}" \;) \ + ${info_files[@]/#/-a } \ + ${json_files[@]/#/-j } \ -o $merged_coverage_file \ -m $merged_json_file \ $lc @@ -284,6 +290,7 @@ merge_files() { # Lcov report files for each info file ################################################################ generate_local_reports() { + printf "\tCreating local code coverage reports...\n" for i in ${!info_files[@]}; do local info_file=${info_files[$i]} @@ -303,23 +310,10 @@ generate_local_reports() { # Prints to stdout script usage ################################################################ usage() { - clear - echo "Usage:" - echo "merge -h Display this help message." - echo "-j JSON configuration file (info and intermediate json filenames to be merged)." - echo "[-l ] Coverage reports directory. Defaults to ./Coverage" - echo "[-w ] Workspace directory for source code files." - echo "[-o ] Merged info file. Defaults to ./merged_coverage.info" - echo "[-m ] JSON merged SCM sources. Defaults to ./merged_scm.json" - echo "[-c] Flag to download/copy the source files from the JSON merged SCM into the workspace directory." - echo "[-g] Flag to generate local reports for each info/json instance." - echo "$help_message" -} - help_message=$(cat <: Metadata that can be used for print more information related to +# each project [Optional] # Example: { "files" : [ { @@ -344,7 +340,8 @@ help_message=$(cat <" - } + }, + "metadata": .... }, { "id": "", @@ -357,7 +354,8 @@ help_message=$(cat <" - } + }, + "metadata": .... }, . . @@ -366,8 +364,21 @@ help_message=$(cat < JSON configuration file (info and intermediate json filenames to be merged)." + echo "[-l ] Coverage reports directory. Defaults to ./Coverage" + echo "[-w ] Workspace directory for source code files." + echo "[-o ] Merged info file. Defaults to ./merged_coverage.info" + echo "[-m ] JSON merged SCM sources. Defaults to ./merged_scm.json" + echo "[-c] Flag to download/copy the source files from the JSON merged SCM into the workspace directory." + echo "[-g] Flag to generate local reports for each info/json instance." + echo "$help_message" +} + -clear +[ ${-/x} != ${-} ] && TRACING=true || TRACING=false LOCAL_WORKSPACE="" CLONE_SOURCES=false merge_configuration_file="" @@ -377,7 +388,11 @@ LCOV_FOLDER="./Coverage" # File name for merge coverage info merged_coverage_file="./merged_coverage.info" merged_json_file="./merged_scm.json" +# File name to pass variables to calling script +variables_file="./variables.sh" info_files=() # Array of info files +json_files=() # Array of configuration json files +list_of_merged_builds=() while getopts ":hj:o:l:w:i:cm:g" opt; do case ${opt} in h ) @@ -385,7 +400,7 @@ while getopts ":hj:o:l:w:i:cm:g" opt; do exit 0 ;; w ) - LOCAL_WORKSPACE=$(cd $OPTARG; pwd) + LOCAL_WORKSPACE=$(cd $OPTARG &>/dev/null || true; pwd) ;; c ) CLONE_SOURCES=true @@ -405,6 +420,9 @@ while getopts ":hj:o:l:w:i:cm:g" opt; do g ) generate_local=true ;; + x ) + variables_file=$OPTARG + ;; \? ) echo "Invalid option: $OPTARG" 1>&2 usage @@ -440,9 +458,14 @@ if [ $CLONE_SOURCES = true ];then clone_repos $merged_json_file fi -# Generate branch coverage report +# Generate merged coverage report +merged_status=true genhtml --branch-coverage $merged_coverage_file \ --output-directory $LCOV_FOLDER +if [ $? -ne 0 ];then + merged_status=false + echo "ERROR: Cannot merge coverage reports" +fi if [ "$generate_local" = true ];then generate_local_reports