blob: 933f6de57234dc0a03762afd7a9ac9f561c91117 [file] [log] [blame]
Basil Eljuse4b14afb2020-09-30 13:07:23 +01001# !/usr/bin/env python
2###############################################################################
3# Copyright (c) 2020, ARM Limited and Contributors. All rights reserved.
4#
5# SPDX-License-Identifier: BSD-3-Clause
6###############################################################################
7
8###############################################################################
9# FILE: merge.py
10#
11# DESCRIPTION: Merge two or more .info and json files, sanitizing source file
12# paths.
13# If different .info files contain the same source code duplicated
14# in different directories, we use the absolute paths of the
15# first .info file.
16#
17###############################################################################
18
19
20import os
21import sys
22import argparse
23from argparse import RawTextHelpFormatter
24import subprocess
25import json
26
27
28# Define an argument parser using the argparse library
29parser = argparse.ArgumentParser(epilog="""Example of usage:
30python3 merge.py -a coverage_1.info -a coverage_2.info -o coverage_merge.info \
31-j input_file1.json -j input_file2.json -m merge_file.json
32
Saul Romero884d2142023-01-16 10:31:22 +000033The metadata json file must contain the information for every repo that is
34used to build the binaries that were tested (and where coverage is desired).
35As a minimum this file must look like this:
36{
37 "configuration" : {
38 "sources": [
39 {
40 "COMMIT": "XXXXXXX", # [optional]
41 "LOCATION": "YYY", # Folder where the 'URL' repo is cloned in the
42 test workspace.
43 "LOCAL": "ZZZZ", # Local folder for the repo cloned at
44 the local workspace (optional, if not defined 'LOCATION' is assumed).
45 "REFSPEC": "XXXXXX", # [optional]
46 "URL": "XXXXXXXX",
47 "type": "git"
48 }
49 ]
50 }
51}
Basil Eljuse4b14afb2020-09-30 13:07:23 +010052It is possible to merge any number of files at once.
53If metadata json files are defined then they must pair with their
54corresponding info file, i.e. have the same name.
55If a local workspace is defined then the paths in the info files will
56be translated from the original test workspace to the local workspace
57to enable the usage of LCOV, but the original files will be kept intact.
58By default, the output file must be a new file.
59To overwrite an existing file, use the "--force" option.
60
Saul Romero884d2142023-01-16 10:31:22 +000061Note: the user is expected to merge .info files referring to the same
62project, i.e. same sources. If merging .info files from different projects,
63LCOV can be exploited directly using a command such as "lcov -rc
64lcov_branch_coverage=1 -a coverage_1.info \
Basil Eljuse4b14afb2020-09-30 13:07:23 +010065-a coverage_2.info -o coverage_merge.info."
66""", formatter_class=RawTextHelpFormatter)
67requiredNamed = parser.add_argument_group('required named arguments')
68requiredNamed.add_argument("-a", "--add-file",
69 help="Input info file to be merged.",
70 action='append', required=True)
71requiredNamed.add_argument("-o", "--output",
72 help="Name of the output info (merged) file.",
73 required=False)
74parser.add_argument("-j", "--json-file", action='append',
75 help="Input json file to be merged.")
76parser.add_argument("-m", "--output-json",
77 help="Name of the output json (merged) file.")
78parser.add_argument("--force", dest='force', action='store_true',
79 help="force overwriting of output file.")
80parser.add_argument("--local-workspace", dest='local_workspace',
81 help='Local workspace where source files reside.')
Saul Romero884d2142023-01-16 10:31:22 +000082parser.add_argument("-k", action='store_true', dest='keep_trans',
83 help='Keeps translated info files')
84
Basil Eljuse4b14afb2020-09-30 13:07:23 +010085
86options = parser.parse_args(sys.argv[1:])
87# At least two .info files are expected
88if len(options.add_file) < 2:
89 print('Error: too few input files.\n')
90 sys.exit(1)
91# The same number of info and json files expected
92if options.json_file:
93 if len(options.json_file) != len(options.add_file):
Saul Romero884d2142023-01-16 10:31:22 +000094 print('Unmatched number of info and json files.\n')
Basil Eljuse4b14afb2020-09-30 13:07:23 +010095 sys.exit(1)
96
97file_groups = []
98info_files_to_merge = []
99# Check if files exist
100for file_name in options.add_file:
101 print("Merging '{}'".format(file_name))
102 if not os.path.isfile(file_name):
103 print('Error: file "' + file_name + '" not found.\n')
104 sys.exit(1)
105 if not file_name[-5:] == '.info':
106 print('Error: file "' + file_name +
107 '" has wrong extension. Expected .info file.\n')
108 sys.exit(1)
109 if file_name in info_files_to_merge:
110 print("Error: Duplicated info file '{}'".format(file_name))
111 sys.exit(1)
Saul Romero Dominguezfd4d0c92023-02-15 10:47:59 +0000112 if os.stat(file_name).st_size == 0:
113 print("Warning: Empty info file '{}', skipping it".format(file_name))
114 continue
Basil Eljuse4b14afb2020-09-30 13:07:23 +0100115 info_files_to_merge.append(file_name)
116 file_group = {"info": file_name, "locations": [], "json": ""}
117 info_name = os.path.basename(file_name).split(".")[0]
118 if options.json_file:
119 json_name = [i for i in options.json_file
120 if os.path.basename(i).split(".")[0] == info_name]
121 if not json_name:
Saul Romero884d2142023-01-16 10:31:22 +0000122 print("Unmatched json file name for '{}'".format(file_name))
Basil Eljuse4b14afb2020-09-30 13:07:23 +0100123 sys.exit(1)
124 json_name = json_name.pop()
125 if not json_name[-5:] == '.json':
126 print('Error: file "' + json_name +
127 '" has wrong extension. Expected .json file.\n')
128 sys.exit(1)
129 if not os.path.isfile(json_name):
130 print('Error: file "' + json_name + '" not found.\n')
131 sys.exit(1)
132 # Now we have to extract the location folders for each info
133 # this is needed if we want translation to local workspace
134 file_group["json"] = json_name
135 with open(json_name) as json_file:
136 json_data = json.load(json_file)
137 locations = []
Saul Romero Dominguezfd4d0c92023-02-15 10:47:59 +0000138 parent = json_data.get("parameters", json_data.get("configuration"))
139 for source in parent["sources"]:
Saul Romero884d2142023-01-16 10:31:22 +0000140 location = source["LOCATION"]
141 locations.append((location, source.get("LOCAL", location)))
Basil Eljuse4b14afb2020-09-30 13:07:23 +0100142 file_group["locations"] = locations
143 file_groups.append(file_group)
144
145# Check the extension of the output file
146if not options.output[-5:] == '.info':
147 print('Error: file "' + options.output +
148 '" has wrong extension. Expected .info file.\n')
149 sys.exit(1)
150
151if options.local_workspace is not None:
152 # Translation from test to local workspace
153 i = 0
154 while i < len(info_files_to_merge):
155 info_file = open(info_files_to_merge[i], "r")
156 print("Translating workspace for '{}'...".format(
157 info_files_to_merge[i]))
158 info_lines = info_file.readlines()
159 info_file.close()
Saul Romero884d2142023-01-16 10:31:22 +0000160 temp_file = info_file.name.replace('.info', '_local.info')
161 if options.keep_trans:
162 print("Keeping translated info file {}...".format(temp_file))
saul-romero-arm6c40b242021-06-18 10:21:04 +0000163 parts = None
Basil Eljuse4b14afb2020-09-30 13:07:23 +0100164 with open(temp_file, "w+") as f:
165 for line in info_lines:
saul-romero-arm6c40b242021-06-18 10:21:04 +0000166 if "SF" in line:
167 for location in file_groups[i]["locations"]:
Saul Romero884d2142023-01-16 10:31:22 +0000168 if location[0] in line:
169 parts = line[3:].partition(location[0] + "/")
170 local_name = location[1]
171 line = line[:3] + os.path.join(
172 options.local_workspace, location[1], parts[2])
saul-romero-arm6c40b242021-06-18 10:21:04 +0000173 break
174 f.write(line)
Basil Eljuse4b14afb2020-09-30 13:07:23 +0100175 info_files_to_merge[i] = temp_file # Replace info file to be merged
176 i += 1
177
178# Merge json files
Saul Romero884d2142023-01-16 10:31:22 +0000179if options.json_file and len(options.json_file):
Basil Eljuse4b14afb2020-09-30 13:07:23 +0100180 json_merged_list = []
181 json_merged = {}
182 j = 0
183 while j < len(options.json_file):
184 json_file = options.json_file[j]
185 with open(json_file) as f:
186 data = json.load(f)
Saul Romero Dominguezfd4d0c92023-02-15 10:47:59 +0000187 parent = data.get("parameters", data.get("configuration"))
188 for source in parent['sources']:
Basil Eljuse4b14afb2020-09-30 13:07:23 +0100189 if source not in json_merged_list:
190 json_merged_list.append(source)
191 j += 1
192 json_merged = {'configuration': {'sources': json_merged_list}}
193 with open(options.output_json, 'w') as f:
Saul Romero884d2142023-01-16 10:31:22 +0000194 json.dump(json_merged, f, indent=4)
Basil Eljuse4b14afb2020-09-30 13:07:23 +0100195
196
197# Exploit LCOV merging capabilities
198# Example of LCOV usage: lcov -rc lcov_branch_coverage=1 -a coverage_1.info \
199# -a coverage_2.info -o coverage_merge.info
Saul Romeroaed264c2024-04-02 10:20:51 +0000200command = ['lcov', '--rc', 'lcov_branch_coverage=1']
Basil Eljuse4b14afb2020-09-30 13:07:23 +0100201
202for file_name in info_files_to_merge:
203 command.append('-a')
204 command.append(file_name)
205command.append('-o')
206command.append(options.output)
207
208subprocess.call(command)
209
210# Delete the temporary files
Saul Romero884d2142023-01-16 10:31:22 +0000211if options.local_workspace is not None and not options.keep_trans:
Basil Eljuse4b14afb2020-09-30 13:07:23 +0100212 for f in info_files_to_merge:
213 os.remove(f)