blob: 1c79db0b747cfc2956d4d287fcf7c442293d70bb [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (c) 2020-2021, Arm Limited. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""
This module can map the coverage of c-picker generated files to the original source files.
"""
import argparse
import json
import os
import re
import sys
class MappingDescriptor:
""" Storage class for file-line number data """
def __init__(self, file=None, line=None):
self.descriptor = {"file": file, "line": line}
def get_file(self):
""" Queries file name """
return self.descriptor["file"]
def get_line(self):
""" Queries line number """
return self.descriptor["line"]
@staticmethod
def serialize(mapping_descriptor):
""" Serializes the descriptor into a C comment containing JSON. """
return "/* C-PICKER-MAPPING " + json.dumps(mapping_descriptor.descriptor) + " */"
@staticmethod
def deserialize(line):
"""
Deserializes the descriptor from a C comment containing JSON.
It returns None if the line is not matching the required pattern.
"""
match = re.match(r"/\* C-PICKER-MAPPING ({.*}) \*/", line)
if not match:
return None
mapping_descriptor = MappingDescriptor()
mapping_descriptor.descriptor = json.loads(match.group(1))
return mapping_descriptor
class CoverageMapper:
""" The class maps the coverage of the c-picker generated source files to the original ones. """
def __init__(self):
self.mapping_path = None
self.output = sys.stdout
self.test_name = None
self.mapping_enabled = False
self.mapping_descriptors = {}
self.mapped_source_file = None
def read_mapping_descriptors(self, filename):
""" Reads the mapping descriptors from the c-picker generated file. """
self.mapping_enabled = True
with open(filename, "r") as source_file:
source_lines = source_file.read().split("\n")
source_line_index = 1
for source_line in source_lines:
mapping_descriptor = MappingDescriptor.deserialize(source_line)
if mapping_descriptor:
# +1: the elements start at the following line after the descriptor comment
self.mapping_descriptors[source_line_index + 1] = mapping_descriptor
source_line_index += 1
def clear_mapping_descriptors(self):
""" Resets the mapping descriptor database. """
self.mapping_enabled = False
self.mapping_descriptors = {}
self.mapped_source_file = None
def find_mapping(self, line_number):
""" Find the mapping descriptor of a line number and also returns the mapped line. """
mapping_descriptor_index = 0
for i in self.mapping_descriptors:
if i > line_number:
break
mapping_descriptor_index = i
if not mapping_descriptor_index:
raise Exception("Invalid mapping for line %d" % line_number)
mapping_descriptor = self.mapping_descriptors[mapping_descriptor_index]
mapped_line = line_number - mapping_descriptor_index + mapping_descriptor.get_line()
return mapping_descriptor, mapped_line
def output_line(self, line):
""" Outputs a single line to the output """
self.output.write(line + "\n")
def process_line(self, trace_line):
"""
The function processes a single line of the trace file and maintains the internal state of
the state matchine.
"""
if not trace_line:
return
if trace_line == "end_of_record":
# End of record, exit mapping mode
self.clear_mapping_descriptors()
self.output_line(trace_line)
return
command, params = trace_line.split(":", 1)
if command == "TN":
# Test name TN:<test name>
self.test_name = params
elif command == "SF" and params.startswith(self.mapping_path):
# Source file SF:<absolute path to the source file>
# Matching source file, switch into mapping mode
self.read_mapping_descriptors(params)
return
if self.mapping_enabled and (command in ("FN", "BRDA", "DA")):
# Function FN:<line number of function start>,<function name>
# Branch coverage BRDA:<line number>,<block number>,<branch number>,<taken>
# Line coverage DA:<line number>,<execution count>[,<checksum>]
line_number, remaining_params = params.split(",", 1)
mapping_descriptor, mapped_line_number = self.find_mapping(int(line_number))
if mapping_descriptor.get_file() != self.mapped_source_file:
# Change in the name of the mapped source file, starting new record
if self.mapped_source_file is not None:
# No need for this part if it was the first mapped file
# because TN has been alread printed
self.output_line("end_of_record")
self.output_line("TN:%s" % self.test_name)
self.mapped_source_file = mapping_descriptor.get_file()
self.output_line("SF:%s" % self.mapped_source_file)
self.output_line("%s:%d,%s" % (command, mapped_line_number, remaining_params))
return
self.output_line(trace_line)
def main(self):
""" Runs coverage mapper configured by the command line arguments """
try:
parser = argparse.ArgumentParser(prog="c-picker-coverage-mapper", description=__doc__)
parser.add_argument("--input", help="Input trace file", required=True)
parser.add_argument("--output", help="Input file")
parser.add_argument("--mapping-path", help="Directory of generated files",
required=True)
args = parser.parse_args()
output_fp = open(args.output, "w") if args.output else None
self.output = output_fp if output_fp else sys.stdout
self.mapping_path = os.path.abspath(args.mapping_path)
with open(args.input, "r") as tracefile:
trace_lines = tracefile.read().split("\n")
for trace_line in trace_lines:
self.process_line(trace_line)
if output_fp:
output_fp.close()
return 0
except FileNotFoundError as exception:
print("File not found: %s" % str(exception), file=sys.stderr)
except ValueError as exception:
print("Invalid format: %s" % str(exception), file=sys.stderr)
except Exception as exception: # pylint: disable=broad-except
print("Exception: %s" % exception, file=sys.stderr)
return 1
def main():
""" Command line main function """
coverage_mapper = CoverageMapper()
result = coverage_mapper.main()
sys.exit(result)
if __name__ == "__main__":
main()