Skip to content

Commit 08d97f1

Browse files
coverage.py: Add script to process code coverage
This script ease the generation of code coverage report. A report is based on a codebase, note files (generated at compilation) and data files (generated at runtime). The report is generated using lcov and present the code coverage as a percentage of lines executed compared to the total number of lines in the codebase. The script will copy all the note and data files in a temporary directory allowing to reuse the generated artifact and checks that the note and data file are correctly organised (the files must be in the same directory). The script also allow to generate an html output using genhtml and to view it. Signed-off-by: Franck LENORMAND <[email protected]>
1 parent 92afa01 commit 08d97f1

File tree

1 file changed

+230
-0
lines changed

1 file changed

+230
-0
lines changed

scripts/coverage.py

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#!/usr/bin/env python3
2+
3+
# This script will create the coverage report given the coverage notes and data
4+
5+
import sys # Handle exit status
6+
import os # Interact with PATH env variable and filesystem
7+
import argparse # Handle command line
8+
import logging # Logging
9+
import distutils.dir_util # Copy of tree
10+
import subprocess # Call lcov and genhtml
11+
from pathlib import Path # Glob on gcda and gcno
12+
13+
14+
def find_binary(logger, bin_name, proposed_bin_path):
15+
"""
16+
Finds a binary.
17+
18+
:param bin_name: The bin name
19+
:type bin_name: string
20+
21+
Return the path to the binary
22+
"""
23+
24+
# Check path passed as argument
25+
if proposed_bin_path is not None:
26+
if os.path.isfile(proposed_bin_path):
27+
return proposed_bin_path
28+
29+
# Get PATH variable
30+
pathvar = os.environ['PATH']
31+
paths = pathvar.split(os.pathsep)
32+
33+
# Loop through paths
34+
for path in paths:
35+
filepath = os.path.join(path, bin_name)
36+
37+
# Try to find the file
38+
if os.path.isfile(filepath):
39+
return filepath
40+
41+
logger.warning("Could not find {} in {}".format(bin_name, paths))
42+
43+
# Raise exception
44+
raise FileNotFoundError('{}'.format(bin_name))
45+
46+
47+
def main():
48+
cwd = os.getcwd()
49+
50+
logging.basicConfig(level=logging.INFO)
51+
logger = logging.getLogger("coverage")
52+
53+
default_gcov = "gcov"
54+
default_lcov = "lcov"
55+
default_genhtml = "genhtml"
56+
default_browser = "firefox"
57+
58+
p = argparse.ArgumentParser(description='Process coverage data generated \
59+
to create coverage result along with an HTML \
60+
output')
61+
62+
g1 = p.add_argument_group("Information about the analysis")
63+
g1.add_argument("--uc", help="Name of the Use Case", type=str,
64+
default="my_uc")
65+
g1.add_argument("--info", help="Name of the info file to create", type=str)
66+
g1.add_argument("--outdir", help="Path to the directory for analysis",
67+
type=str)
68+
g1.add_argument("--html", help="Generate HTML output", action='store_true')
69+
g1.add_argument("--view", help="View HTML output in browser",
70+
action='store_true')
71+
g1.add_argument("--src_dir", help="Top dir of the sources", type=str,
72+
required=True)
73+
74+
g2 = p.add_argument_group("Coverage material generated")
75+
g2.add_argument("--notes", help="Directory containing the coverage notes",
76+
type=str, required=True)
77+
g2.add_argument("--data", help="Directory containing the coverage data",
78+
type=str, required=True)
79+
80+
g3 = p.add_argument_group("External tools")
81+
g3.add_argument("--gcov", help="Path to the gcov binary", type=str,
82+
default=default_gcov)
83+
g3.add_argument("--lcov", help="Path to lcov binary", type=str,
84+
default=default_lcov)
85+
g3.add_argument("--genhtml", help="Path to genhtml binary", type=str,
86+
default=default_genhtml)
87+
g3.add_argument("--browser", help="Path to browser binary", type=str,
88+
default=default_browser)
89+
90+
p.add_argument("-v", "--verbose", help="verbose mode", action='store_true')
91+
92+
# Parse the command line
93+
args = p.parse_args()
94+
95+
if args.verbose:
96+
logger.setLevel(logging.DEBUG)
97+
98+
logger.debug("Check analysis parameters")
99+
logger.info("Use case name: {}".format(args.uc))
100+
101+
dir_analysis = (args.outdir if args.outdir is not None
102+
else "{}/{}".format(cwd, args.uc))
103+
logger.info("Analysis directory: {}".format(dir_analysis))
104+
105+
info_filename = (args.info if args.info is not None
106+
else "{}.info".format(args.uc))
107+
logger.info("Information filename: {}".format(info_filename))
108+
109+
info_filepath = "{}/{}".format(dir_analysis, info_filename)
110+
111+
logger.debug("Check coverage notes directory")
112+
if not os.path.isdir(args.notes):
113+
raise NotADirectoryError(args.notes)
114+
logger.info("Directory containing notes: {}".format(args.notes))
115+
116+
logger.debug("Check coverage data directory")
117+
if not os.path.isdir(args.data):
118+
raise NotADirectoryError(args.data)
119+
logger.info("Directory containing data: {}".format(args.data))
120+
121+
logger.debug("Check source directory")
122+
if not os.path.isdir(args.src_dir):
123+
raise NotADirectoryError(args.src_dir)
124+
logger.info("Directory of sources: {}".format(args.src_dir))
125+
126+
logger.debug("Check external tools")
127+
logger.debug("Set gcov binary")
128+
gcov_bin = find_binary(l, default_gcov, args.gcov)
129+
logger.info("Binary for gcov: {}".format(gcov_bin))
130+
131+
logger.debug("Set lcov binary")
132+
lcov_bin = find_binary(l, default_lcov, args.lcov)
133+
logger.info("Binary for lcov: {}".format(lcov_bin))
134+
135+
if args.html:
136+
logger.debug("Set genhtml binary")
137+
genhtml_bin = find_binary(l, default_genhtml, args.genhtml)
138+
logger.info("Binary for genhtml: {}".format(genhtml_bin))
139+
140+
if args.view:
141+
logger.debug("Set browser binary")
142+
browser_bin = find_binary(l, default_browser, args.genhtml)
143+
logger.info("Binary for genhtml: {}".format(genhtml_bin))
144+
145+
logger.debug("Create analysis directory")
146+
os.mkdir(dir_analysis)
147+
logger.info("Analysis directory: {}".format(dir_analysis))
148+
149+
logger.debug("Copy notes")
150+
distutils.dir_util.copy_tree(args.notes, dir_analysis)
151+
logger.debug("Copy notes done")
152+
153+
logger.debug("Copy data")
154+
distutils.dir_util.copy_tree(args.data, dir_analysis)
155+
logger.debug("Copy data done")
156+
157+
logger.debug("Check that the gcno are along the gcda")
158+
logger.debug("Search for all the gcda")
159+
gcda_files = list(Path(dir_analysis).rglob('*.gcda'))
160+
161+
if not gcda_files:
162+
raise FileNotFoundError("No gcda files can be found in {}".
163+
format(dir_analysis))
164+
165+
for gcda_file in gcda_files:
166+
component = Path(gcda_file).with_suffix('')
167+
gcno_file_exp = str(component) + ".gcno"
168+
logger.debug("gcda: {}, looking for {}".format(gcda_file,
169+
gcno_file_exp))
170+
171+
if not os.path.isfile(gcno_file_exp):
172+
logger.error("{} does not exists".format(gcno_file_exp))
173+
gcno_file = os.path.basename(gcno_file_exp)
174+
gcno_files = list(Path(dir_analysis).rglob(gcno_file))
175+
176+
if not gcno_files:
177+
raise FileNotFoundError("{} could not be found in {}".
178+
format(gcno_file, dir_analysis))
179+
else:
180+
raise FileNotFoundError('{} not along {}, located at {}'.
181+
format(gcno_file_exp, gcda_file,
182+
gcno_files[0]))
183+
184+
run_analysis_cmd = [lcov_bin, "--capture", "--directory", dir_analysis,
185+
"--gcov-tool", gcov_bin, "--output-file",
186+
info_filepath]
187+
logger.debug("Analysis command: {}".format(run_analysis_cmd))
188+
189+
logger.debug("Run the analysis")
190+
run_analysis = subprocess.check_output(run_analysis_cmd,
191+
stderr=subprocess.STDOUT)
192+
193+
gcov_errors = ["skipping", "did not produce any data for"]
194+
195+
logger.debug("Check analysis output")
196+
for line in run_analysis.decode("utf-8").splitlines():
197+
for error in gcov_errors:
198+
if error in line:
199+
raise RuntimeError(line)
200+
201+
logger.info("Analysis done, information file: {}".format(info_filepath))
202+
203+
if args.html:
204+
html_folder = "{}/html".format(dir_analysis)
205+
206+
generate_html_cmd = [genhtml_bin, "--prefix", args.src_dir, "--legend",
207+
"--title", args.uc, "--output-directory",
208+
html_folder, info_filepath]
209+
logger.debug("HTML generation command: {}".format(run_analysis_cmd))
210+
211+
logger.debug("Create HTML output")
212+
generate_html = subprocess.check_output(generate_html_cmd)
213+
214+
logger.info("HTML generation done, output: {}".format(html_folder))
215+
216+
html_index = "{}/index.html".format(html_folder)
217+
logger.info("HTML index file: {}".format(html_index))
218+
219+
if args.view:
220+
view_cmd = [browser_bin, html_index]
221+
222+
logger.debug("Show HTML output in browser")
223+
logger.info("View HTML in browser: {}".format(view_cmd))
224+
subprocess.run(view_cmd)
225+
226+
sys.exit(0)
227+
228+
229+
if __name__ == "__main__":
230+
main()

0 commit comments

Comments
 (0)