blob: 1c79db0b747cfc2956d4d287fcf7c442293d70bb [file] [log] [blame]
Imre Kis124f0e32021-02-12 18:03:24 +01001#!/usr/bin/env python3
2# Copyright (c) 2020-2021, Arm Limited. All rights reserved.
3#
4# SPDX-License-Identifier: BSD-3-Clause
5
6"""
7This module can map the coverage of c-picker generated files to the original source files.
8"""
9
10import argparse
11import json
12import os
13import re
14import sys
15
16class MappingDescriptor:
17 """ Storage class for file-line number data """
18
19 def __init__(self, file=None, line=None):
20 self.descriptor = {"file": file, "line": line}
21
22 def get_file(self):
23 """ Queries file name """
24 return self.descriptor["file"]
25
26 def get_line(self):
27 """ Queries line number """
28 return self.descriptor["line"]
29
30 @staticmethod
31 def serialize(mapping_descriptor):
32 """ Serializes the descriptor into a C comment containing JSON. """
33 return "/* C-PICKER-MAPPING " + json.dumps(mapping_descriptor.descriptor) + " */"
34
35 @staticmethod
36 def deserialize(line):
37 """
38 Deserializes the descriptor from a C comment containing JSON.
39 It returns None if the line is not matching the required pattern.
40 """
41 match = re.match(r"/\* C-PICKER-MAPPING ({.*}) \*/", line)
42 if not match:
43 return None
44
45 mapping_descriptor = MappingDescriptor()
46 mapping_descriptor.descriptor = json.loads(match.group(1))
47 return mapping_descriptor
48
49class CoverageMapper:
50 """ The class maps the coverage of the c-picker generated source files to the original ones. """
51
52 def __init__(self):
53 self.mapping_path = None
54 self.output = sys.stdout
55
56 self.test_name = None
57 self.mapping_enabled = False
58 self.mapping_descriptors = {}
59 self.mapped_source_file = None
60
61 def read_mapping_descriptors(self, filename):
62 """ Reads the mapping descriptors from the c-picker generated file. """
63 self.mapping_enabled = True
64
65 with open(filename, "r") as source_file:
66 source_lines = source_file.read().split("\n")
67 source_line_index = 1
68
69 for source_line in source_lines:
70 mapping_descriptor = MappingDescriptor.deserialize(source_line)
71
72 if mapping_descriptor:
73 # +1: the elements start at the following line after the descriptor comment
74 self.mapping_descriptors[source_line_index + 1] = mapping_descriptor
75 source_line_index += 1
76
77 def clear_mapping_descriptors(self):
78 """ Resets the mapping descriptor database. """
79 self.mapping_enabled = False
80 self.mapping_descriptors = {}
81 self.mapped_source_file = None
82
83 def find_mapping(self, line_number):
84 """ Find the mapping descriptor of a line number and also returns the mapped line. """
85 mapping_descriptor_index = 0
86 for i in self.mapping_descriptors:
87 if i > line_number:
88 break
89 mapping_descriptor_index = i
90
91 if not mapping_descriptor_index:
92 raise Exception("Invalid mapping for line %d" % line_number)
93
94 mapping_descriptor = self.mapping_descriptors[mapping_descriptor_index]
95 mapped_line = line_number - mapping_descriptor_index + mapping_descriptor.get_line()
96 return mapping_descriptor, mapped_line
97
98 def output_line(self, line):
99 """ Outputs a single line to the output """
100 self.output.write(line + "\n")
101
102 def process_line(self, trace_line):
103 """
104 The function processes a single line of the trace file and maintains the internal state of
105 the state matchine.
106 """
107 if not trace_line:
108 return
109
110 if trace_line == "end_of_record":
111 # End of record, exit mapping mode
112 self.clear_mapping_descriptors()
113 self.output_line(trace_line)
114 return
115
116 command, params = trace_line.split(":", 1)
117 if command == "TN":
118 # Test name TN:<test name>
119 self.test_name = params
120 elif command == "SF" and params.startswith(self.mapping_path):
121 # Source file SF:<absolute path to the source file>
122 # Matching source file, switch into mapping mode
123 self.read_mapping_descriptors(params)
124 return
125
126 if self.mapping_enabled and (command in ("FN", "BRDA", "DA")):
127 # Function FN:<line number of function start>,<function name>
128 # Branch coverage BRDA:<line number>,<block number>,<branch number>,<taken>
129 # Line coverage DA:<line number>,<execution count>[,<checksum>]
130 line_number, remaining_params = params.split(",", 1)
131 mapping_descriptor, mapped_line_number = self.find_mapping(int(line_number))
132
133 if mapping_descriptor.get_file() != self.mapped_source_file:
134 # Change in the name of the mapped source file, starting new record
135 if self.mapped_source_file is not None:
136 # No need for this part if it was the first mapped file
137 # because TN has been alread printed
138 self.output_line("end_of_record")
139 self.output_line("TN:%s" % self.test_name)
140 self.mapped_source_file = mapping_descriptor.get_file()
141 self.output_line("SF:%s" % self.mapped_source_file)
142
143 self.output_line("%s:%d,%s" % (command, mapped_line_number, remaining_params))
144 return
145
146 self.output_line(trace_line)
147
148 def main(self):
149 """ Runs coverage mapper configured by the command line arguments """
150
151 try:
152 parser = argparse.ArgumentParser(prog="c-picker-coverage-mapper", description=__doc__)
153 parser.add_argument("--input", help="Input trace file", required=True)
154 parser.add_argument("--output", help="Input file")
155 parser.add_argument("--mapping-path", help="Directory of generated files",
156 required=True)
157 args = parser.parse_args()
158
159 output_fp = open(args.output, "w") if args.output else None
160
161 self.output = output_fp if output_fp else sys.stdout
162 self.mapping_path = os.path.abspath(args.mapping_path)
163
164 with open(args.input, "r") as tracefile:
165 trace_lines = tracefile.read().split("\n")
166 for trace_line in trace_lines:
167 self.process_line(trace_line)
168
169 if output_fp:
170 output_fp.close()
171
172 return 0
173 except FileNotFoundError as exception:
174 print("File not found: %s" % str(exception), file=sys.stderr)
175 except ValueError as exception:
176 print("Invalid format: %s" % str(exception), file=sys.stderr)
177 except Exception as exception: # pylint: disable=broad-except
178 print("Exception: %s" % exception, file=sys.stderr)
179
180 return 1
181
182def main():
183 """ Command line main function """
184 coverage_mapper = CoverageMapper()
185 result = coverage_mapper.main()
186 sys.exit(result)
187
188if __name__ == "__main__":
189 main()