blob: 1a851b87fc6f15d0b8dfe204de14e49ce6c6f945 [file] [log] [blame]
# !/usr/bin/env python
###############################################################################
# Copyright (c) 2020-2023, ARM Limited and Contributors. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
###############################################################################
###############################################################################
# FILE: intermediate_layer.py
#
# DESCRIPTION: Creates an intermediate json file with information provided
# by the configuration json file, dwarf signatures and trace
# files.
#
###############################################################################
import os
import re
import glob
import argparse
import subprocess
import json
from argparse import RawTextHelpFormatter
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
import logging
__version__ = "7.0"
# Static map that defines the elf file source type in the intermediate json
ELF_MAP = {
"bl1": 0,
"bl2": 1,
"bl31": 2,
"bl32": 3,
"scp_ram": 10,
"scp_rom": 11,
"mcp_rom": 12,
"mcp_ram": 13,
"secure_hafnium": 14,
"hafium": 15,
"custom_offset": 100
}
def os_command(command, show_command=False):
"""
Function that execute an os command, on fail exit the program
:param command: OS command as string
:param show_command: Optional argument to print the command in stdout
:return: The string output of the os command
"""
try:
if show_command:
print("OS command: {}".format(command))
out = subprocess.check_output(
command, stderr=subprocess.STDOUT, shell=True)
except subprocess.CalledProcessError as ex:
raise Exception(
"Exception running command '{}': {}({})".format(
command, ex.output, ex.returncode))
return out.decode("utf8")
def load_stats_from_traces(trace_globs):
"""
Function to process and consolidate statistics from trace files
:param trace_globs: List of trace file patterns
:return: Dictionary with stats from trace files i.e.
{mem address in decimal}=(times executed, inst size)
"""
stats = {}
stat_size = {}
# Make a list of unique trace files
trace_files = []
for tg in trace_globs:
trace_files.extend(glob.glob(tg))
trace_files = set(trace_files)
if not trace_files:
raise Exception("No trace files found for '{}'".format(trace_globs))
# Load stats from the trace files
for trace_file in trace_files:
try:
with open(trace_file, 'r') as f:
for line in f:
data = line.split()
address = int(data[0], 16)
stat = int(data[1])
size = int(data[2])
stat_size[address] = size
if address in stats:
stats[address] += stat
else:
stats[address] = stat
except Exception as ex:
logger.error("@Loading stats from trace files:{}".format(ex))
# Merge the two dicts
for address in stats:
stats[address] = (stats[address], stat_size[address])
return stats
def get_code_sections_for_binary(elf_name):
"""
Function to return the ranges of memory address for sections of code
in the elf file
:param elf_name: Elf binary file name
:return: List of code sections tuples, i.e. (section type, initial
address, end address)
"""
command = """%s -h %s | grep -B 1 CODE | grep -v CODE \
| awk '{print $2" "$4" "$3}'""" % (OBJDUMP, elf_name)
text_out = os_command(command)
sections = text_out.split('\n')
sections.pop()
secs = []
for sec in sections:
try:
d = sec.split()
secs.append((d[0], int(d[1], 16), int(d[2], 16)))
except Exception as ex:
logger.error(
"@Returning memory address code sections:".format(ex))
return secs
def get_executable_ranges_for_binary(elf_name):
"""
Get function ranges from an elf file
:param elf_name: Elf binary file name
:return: List of tuples for ranges i.e. (range start, range end)
"""
# Parse all $x / $d symbols
symbol_table = []
address = None
_type = None
command = r"""%s -s %s | awk '/\$[xatd]/ {print $2" "$8}'""" % (
READELF, elf_name)
text_out = os_command(command)
lines = text_out.split('\n')
lines.pop()
for line in lines:
try:
data = line.split()
address = int(data[0], 16)
_type = 'X' if data[1] in ['$x', '$t', '$a'] else 'D'
except Exception as ex:
logger.error("@Getting executable ranges:".format(ex))
symbol_table.append((address, _type))
# Add markers for end of code sections
sections = get_code_sections_for_binary(elf_name)
for sec in sections:
symbol_table.append((sec[1] + sec[2], 'S'))
# Sort by address
symbol_table = sorted(symbol_table, key=lambda tup: tup[0])
# Create ranges (list of START/END tuples)
ranges = []
range_start = symbol_table[0][0]
rtype = symbol_table[0][1]
for sym in symbol_table:
if sym[1] != rtype:
if rtype == 'X':
# Subtract one because the first address of the
# next range belongs to the next range.
ranges.append((range_start, sym[0] - 1))
range_start = sym[0]
rtype = sym[1]
return ranges
def remove_workspace(path, workspace):
"""
Get the relative path to a given workspace
:param path: Path relative to the workspace to be returned
:param workspace: Path.
"""
ret = path if workspace is None else os.path.relpath(path, workspace)
return ret
def get_function_line_numbers(source_file: str) -> Dict[str, int]:
"""
Using ctags get all the function names with their line numbers
within the source_file
:return: Dictionary with function name as key and line number as value
"""
command = "ctags -x --c-kinds=f {}".format(source_file)
fln = {}
try:
function_lines = os_command(command).split("\n")
for line in function_lines:
cols = line.split()
if len(cols) < 3:
continue
if cols[1] == "function":
fln[cols[0]] = 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)
except Exception as ex:
logger.warning(f"Warning: Unknown error '{ex}' when executing command "
f"'{command}'")
return {}
return fln
class FunctionLineNumbers(object):
"""Helper class used to get a function start line number within
a source code file"""
def __init__(self, workspace: str):
"""
Initialise dictionary to allocate source code files with the
corresponding function start line numbers.
:param workspace: The folder where the source files are deployed
"""
self.filenames = {}
self.workspace = workspace
def get_line_number(self, filename: str, function_name: str) -> int:
if not FUNCTION_LINES_ENABLED:
return 0
if filename not in self.filenames:
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]
class BinaryParser(object):
"""Class used to create an instance to parse the binary files with a
dwarf signature in order to produce logical information to be matched with
traces and produce a code coverage report"""
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 _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.no_source_functions = self.get_no_source_functions()
self.workspace = _workspace
self.remove_workspace = _remove_workspace
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 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 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 name and
dwarf data of the block.
"""
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) -> Generator['BinaryParser.FunctionBlock', None,
None]:
"""
Static method generator to extract a function block from the binary
dump.
:param dump: Binary dump (string) that contains the binary file
information.
:return: A FunctionBlock object that is a logical representation
of a function declaration within the binary dump.
"""
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) != 2:
continue
function_group = list(group)
function_group[-1] += "\n\n" # For parsing source code blocks
yield BinaryParser.FunctionBlock(function_group)
@property
def values(self):
return self.name, self.source_file, self.function_line_number
def __str__(self):
return f"'{self.name}:{self.function_line_number}'"
class AssemblyLine(object):
"""Class used to represent an assembly code line within an
assembly code block.
The assembly line instruction is formed by the following components:
- Hexadecimal address of the assembly instruction.
- Assembly instruction.
"""
def __init__(self, line):
"""
Create an instance representing an assembly code line within an
assembly code block.
:param line: Tuple of 2 elements [Hexadecimal number,
and assembly code]
"""
self.hex_line_number, self.opcode = line
self.dec_address = int(self.hex_line_number, 16)
self.times_executed = 0
@staticmethod
def get(asm_code: str) -> Generator['BinaryParser.AssemblyLine',
None, None]:
"""
Static method generator to extract an assembly code line from an
assembly 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", 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)
@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.
:param function_block: FunctionBlock object that contains the code
the source code blocks.
:return: A SourceCodeBlock object.
"""
for source_code_block in BinaryParser.SourceCodeBlock.get(
function_block.dwarf):
if self.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) -> 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):
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(
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 a 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
code coverage in assembly and c source code.
"""
def __init__(self, _config, local_workspace):
self._data = {}
self.config = _config
self.workspace = self.config['parameters']['workspace']
self.remove_workspace = self.config['configuration']['remove_workspace']
self.local_workspace = local_workspace
self.elfs = self.config['elfs']
# Dictionary with stats from trace files {address}=(times executed,
# inst size)
self.traces_stats = {}
# Dictionary of unique assembly line memory address against source
# file location
# {assembly address} = (opcode, source file location, line number in
# the source file, times executed)
self.asm_lines = {}
# Dictionary of {source file location}=>{'lines': {'covered':Boolean,
# 'elf_index'; {elf index}=>{assembly address}=>(opcode,
# times executed),
# 'functions': {function name}=>is covered(boolean)}
self.coverage = CoverageHandler()
self.functions = []
# Unique set of elf list of files
self.elf_map = {}
# For elf custom mappings
self.elf_custom = None
def process(self):
"""
Public method to process the trace files and dwarf signatures
using the information contained in the json configuration file.
This method writes the intermediate json file output linking
the trace data and c source and assembly code.
"""
self.asm_lines = {}
# Initialize for unknown elf files
self.elf_custom = ELF_MAP["custom_offset"]
print("Generating intermediate json layer '{}'...".format(
self.config['parameters']['output_file']))
for elf in self.elfs:
# Gather information
elf_name = elf['name']
# Obtain trace data
self.traces_stats = load_stats_from_traces(elf['traces'])
# Produce code coverage
self._process_binary(elf_name)
# Write to the intermediate json file
data = {"source_files": self.coverage.source_files,
"configuration": {
"sources": self.config['parameters']['sources'],
"metadata": "" if 'metadata' not in
self.config['parameters'] else
self.config['parameters']['metadata'],
"elf_map": self.elf_map}
}
json_data = json.dumps(data, indent=4, sort_keys=True)
with open(self.config['parameters']['output_file'], "w") as f:
f.write(json_data)
def get_elf_index(self, elf_name: str) -> int:
"""Obtains the elf index and fills the elf_map instance variable"""
if elf_name not in self.elf_map:
if elf_name in ELF_MAP:
self.elf_map[elf_name] = ELF_MAP[elf_name]
else:
self.elf_map[elf_name] = ELF_MAP["custom_offset"]
ELF_MAP["custom_offset"] += 1
return self.elf_map[elf_name]
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
"""
command = "%s -Sl %s | tee %s" % (OBJDUMP, elf_filename,
elf_filename.replace(".elf", ".dump"))
dump = os_command(command, show_command=True)
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]
elf_index = self.get_elf_index(elf_name)
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():
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):
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 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
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:
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:
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 = """
Produces an intermediate json layer for code coverage reporting
using an input json configuration file.
Input json configuration file format:
{
"configuration":
{
"remove_workspace": <true if 'workspace' must be from removed from the
path of the source files>,
"include_assembly": <true to include assembly source code in the
intermediate layer>
},
"parameters":
{
"objdump": "<Path to the objdump binary to handle dwarf signatures>",
"readelf: "<Path to the readelf binary to handle dwarf signatures>",
"sources": [ <List of source code origins, one or more of the next
options>
{
"type": "git",
"URL": "<URL git repo>",
"COMMIT": "<Commit id>",
"REFSPEC": "<Refspec>",
"LOCATION": "<Folder within 'workspace' where this source
is located>"
},
{
"type": "http",
"URL": <URL link to file>",
"COMPRESSION": "xz",
"LOCATION": "<Folder within 'workspace' where this source
is located>"
}
],
"workspace": "<Workspace folder where the source code was located to
produce the elf/axf files>",
"output_file": "<Intermediate layer output file name and location>",
"metadata": {<Metadata objects to be passed to the intermediate json
files>}
},
"elfs": [ <List of elf files to be traced/parsed>
{
"name": "<Full path name to elf/axf file>",
"traces": [ <List of trace files to be parsed for this
elf/axf file>
"Full path name to the trace file,"
]
}
]
}
"""
OBJDUMP = None
READELF = None
FUNCTION_LINES_ENABLED = None
def main():
global OBJDUMP
global READELF
global FUNCTION_LINES_ENABLED
parser = argparse.ArgumentParser(epilog=json_conf_help,
formatter_class=RawTextHelpFormatter)
parser.add_argument('--config-json', metavar='PATH',
dest="config_json", default='config_file.json',
help='JSON configuration file', required=True)
parser.add_argument('--local-workspace', default="",
help=('Local workspace folder where source code files'
' and folders resides'))
args = parser.parse_args()
try:
with open(args.config_json, 'r') as f:
config = json.load(f)
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']
# Checking if are installed
os_command("{} --version".format(OBJDUMP))
os_command("{} --version".format(READELF))
if args.local_workspace != "":
# Checking ctags installed
try:
os_command("ctags --version")
except BaseException:
print("Warning!: ctags not installed/working function line numbers\
will be set to 0. [{}]".format(
"sudo apt install exuberant-ctags"))
else:
FUNCTION_LINES_ENABLED = True
intermediate_layer = IntermediateCodeCoverage(config, args.local_workspace)
intermediate_layer.process()
if __name__ == '__main__':
logger = cc_logger.logger
start_time = time.time()
main()
elapsed_time = time.time() - start_time
print("Elapsed time: {}s".format(elapsed_time))