Skip to content

Commit 441b0be

Browse files
committed
Merge pull request #154 from torbjoernk/feature/coverage-script
rewritten coverage report generation script
2 parents 2d5e876 + f486cd3 commit 441b0be

File tree

2 files changed

+264
-98
lines changed

2 files changed

+264
-98
lines changed

generate_coverage.sh

Lines changed: 0 additions & 98 deletions
This file was deleted.

tools/generate_coverage.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#!/usr/bin/env python
2+
# coding=utf-8
3+
"""
4+
This script will aid in generating a test coverage report for PFASST++ including its examples.
5+
6+
A standard CPython 3.3 compatible Python interpreter with standard library support is required.
7+
No additional modules.
8+
9+
Run it with argument `-h` for usage instructions.
10+
11+
.. moduleauthor:: Torbjörn Klatt <[email protected]>
12+
"""
13+
from sys import version_info
14+
# require at least Python 3.3
15+
# (because subprocess.DEVNULL)
16+
assert(version_info[0] >= 3 and version_info[1] >= 3)
17+
18+
19+
import argparse
20+
import os
21+
import os.path
22+
import shutil
23+
import subprocess as sp
24+
import re
25+
import logging
26+
from logging.config import dictConfig
27+
dictConfig(
28+
{
29+
'version': 1,
30+
'disable_existing_loggers': False,
31+
'formatters': {
32+
'default': {
33+
'style': '{',
34+
'format': '[{levelname!s:<8s}] {message!s}'
35+
}
36+
},
37+
'handlers': {
38+
'console': {
39+
'class': 'logging.StreamHandler',
40+
'formatter': 'default'
41+
}
42+
},
43+
'root': {
44+
'handlers': ['console'],
45+
'level': 'INFO'
46+
}
47+
}
48+
)
49+
50+
51+
class Options(object):
52+
coverage_dir = ""
53+
build_dir = ""
54+
base_dir = ""
55+
with_examples = True
56+
tests = []
57+
example_tests = []
58+
tracefiles = []
59+
final_tracefile = ""
60+
61+
62+
options = Options()
63+
options.base_dir = ""
64+
65+
66+
def is_lcov_available():
67+
try:
68+
sp.check_call('lcov --version', shell=True, stdout=sp.DEVNULL, stderr=sp.DEVNULL)
69+
except sp.CalledProcessError:
70+
logging.critical("lcov command not available. It is required.")
71+
return False
72+
73+
try:
74+
sp.check_call('genhtml --version', shell=True, stdout=sp.DEVNULL, stderr=sp.DEVNULL)
75+
except sp.CalledProcessError:
76+
logging.critical("genhtml command not available. It is required.")
77+
return False
78+
79+
return True
80+
81+
82+
def get_project_root():
83+
logging.info("Determine project root directory")
84+
curr_dir = os.path.abspath(os.path.curdir)
85+
logging.debug("Trying current path: %s" % curr_dir)
86+
if os.access(curr_dir + "/include", os.R_OK) and os.access(curr_dir + "/examples", os.R_OK):
87+
logging.debug("Project root is: %s" % curr_dir)
88+
options.base_dir = curr_dir
89+
else:
90+
logging.warning("Probably called from within the tools dir. "
91+
"This should work but is not recommended. "
92+
"Trying parent directory as project root.")
93+
os.chdir("..")
94+
get_project_root()
95+
96+
97+
def setup_and_init_options():
98+
help_string = "Note:\n" \
99+
"This only works for builds made with GCC and the following CMake variables:\n" \
100+
" -Dpfasst_WITH_GCC_PROF=ON -Dpfasst_BUILD_TESTS=ON"
101+
102+
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=help_string)
103+
parser.add_argument('-d', '--build-dir', required=True,
104+
help="name of build directory containing a debug build with GCC and enabled profiling")
105+
parser.add_argument('-o', '--output', default='coverage',
106+
help="output directory for generated coverage report")
107+
parser.add_argument('--no-examples', default=False, action='store_true',
108+
help="whether to not run and include tests from the examples")
109+
parser.add_argument('--debug', default=False, action='store_true',
110+
help="enables more verbose debugging output")
111+
112+
_args = parser.parse_args()
113+
114+
if _args.debug:
115+
logging.getLogger().setLevel(logging.DEBUG)
116+
logging.debug("Debug mode enabled.")
117+
118+
get_project_root()
119+
120+
if not os.access(_args.build_dir, os.W_OK):
121+
logging.critical("Given build path could not be found: %s" % _args.build_dir)
122+
raise ValueError("Given build path could not be found: %s" % _args.build_dir)
123+
options.build_dir = os.path.abspath(_args.build_dir)
124+
125+
if not os.access(_args.output, os.W_OK):
126+
logging.info("Output directory not found. Creating: %s" % _args.output)
127+
os.mkdir(_args.output)
128+
else:
129+
logging.warning("Clearing out output directory: %s" % _args.output)
130+
shutil.rmtree(_args.output)
131+
os.mkdir(_args.output)
132+
options.coverage_dir = os.path.abspath(_args.output)
133+
134+
options.with_examples = not _args.no_examples
135+
if not options.with_examples:
136+
logging.debug("Not running and tracing tests from examples.")
137+
138+
139+
def get_test_directories():
140+
logging.info("Looking for tests ...")
141+
for root, dirs, files in os.walk(options.build_dir + '/tests'):
142+
match_name = re.search('^.*/(?P<test_name>test_[a-zA-Z\-_]+)\.dir$', root)
143+
match_is_example = re.search('^.*/tests/examples/.*$', root)
144+
is_example = match_is_example is not None
145+
if match_name is not None:
146+
testname = match_name.groupdict()['test_name']
147+
if is_example:
148+
options.example_tests.append({'path': root, 'name': testname, 'is_example': is_example})
149+
else:
150+
options.tests.append({'path': root, 'name': testname, 'is_example': is_example})
151+
logging.info("%d tests found" % (len(options.tests) + len(options.example_tests)))
152+
logging.info(" %d general tests" % len(options.tests))
153+
if options.with_examples:
154+
logging.info(" %d tests for examples" % len(options.example_tests))
155+
156+
157+
def run_test(path, name, is_example):
158+
logging.info("- %s" % name)
159+
logging.debug("Found in %s" % path)
160+
output_file = open('%s/%s.log' % (options.coverage_dir, name), mode='a')
161+
logging.debug("Output log: %s" % output_file.name)
162+
163+
os.chdir(os.path.abspath(path))
164+
logging.debug("Deleting old tracing data ...")
165+
print('### deleting old tracing data ...', file=output_file, flush=True)
166+
sp.check_call('lcov --zerocounters --directory .', shell=True, stdout=output_file, stderr=output_file)
167+
print('### done.', file=output_file, flush=True)
168+
169+
os.chdir(options.build_dir)
170+
logging.debug("Running test ...")
171+
print('### running test ...', file=output_file, flush=True)
172+
sp.check_call('ctest -R %s' % name, shell=True, stdout=output_file, stderr=output_file)
173+
print('### done.', file=output_file, flush=True)
174+
175+
os.chdir(os.path.abspath(path))
176+
logging.debug("Capturing all tracing data ...")
177+
print('### capturing all tracing data ...', file=output_file, flush=True)
178+
sp.check_call('lcov --capture --directory . --output-file "%s.info.complete"' % name,
179+
shell=True, stdout=output_file, stderr=output_file)
180+
print('### done.', file=output_file, flush=True)
181+
182+
logging.debug("Removing unnecessary data ...")
183+
print('### removing unnecessary data ...', file=output_file, flush=True)
184+
try:
185+
sp.check_call('lcov --remove "%s.info.complete" "%s/include/pfasst/easylogging++.h" --output-file %s.info.prelim'
186+
% (name, options.base_dir, name),
187+
shell=True, stdout=output_file, stderr=output_file)
188+
except sp.CalledProcessError as e:
189+
logging.warning(e)
190+
print('### done.', file=output_file, flush=True)
191+
192+
logging.debug("Extracting interesting tracing data ...")
193+
print('### extracting interesting tracing data ...', file=output_file, flush=True)
194+
try:
195+
sp.check_call('lcov --extract "%s.info.prelim" "*%s/include/**/*" --output-file %s.info'
196+
% (name, options.base_dir, name),
197+
shell=True, stdout=output_file, stderr=output_file)
198+
options.tracefiles.append("%s/%s.info" % (os.path.abspath(path), name))
199+
except sp.CalledProcessError as e:
200+
logging.warning(e)
201+
if is_example:
202+
logging.debug("This test belongs to an example, thus also covering examples code")
203+
try:
204+
sp.check_call('lcov --extract "%s.info.prelim" "*%s/examples/**/*" --output-file %s.info.example'
205+
% (name, options.base_dir, name),
206+
shell=True, stdout=output_file, stderr=output_file)
207+
options.tracefiles.append("%s/%s.info.example" % (os.path.abspath(path), name))
208+
except sp.CalledProcessError as e:
209+
logging.warning(e)
210+
print('### done.', file=output_file, flush=True)
211+
212+
os.chdir(options.base_dir)
213+
output_file.close()
214+
215+
216+
def run_tests():
217+
logging.info("Running general tests ...")
218+
for test in options.tests:
219+
run_test(**test)
220+
if options.with_examples:
221+
logging.info("Running tests for examples ...")
222+
for example in options.example_tests:
223+
run_test(**example)
224+
225+
226+
def aggregate_tracefiles():
227+
logging.info("Aggregating %d tracefiles ..." % len(options.tracefiles))
228+
output_file = open('%s/aggegrating.log' % (options.coverage_dir,), mode='a')
229+
logging.debug("Output log: %s" % output_file.name)
230+
options.final_tracefile = "%s/all_tests.info" % options.coverage_dir
231+
for tracefile in options.tracefiles:
232+
logging.debug("- %s" % (tracefile))
233+
print("### adding tracefile: %s" % (tracefile,), file=output_file, flush=True)
234+
if os.access(options.final_tracefile, os.W_OK):
235+
sp.check_call('lcov --add-tracefile "%s" --add-tracefile "%s" --output-file "%s"'
236+
% (options.final_tracefile, tracefile, options.final_tracefile),
237+
shell=True, stdout=output_file, stderr=output_file)
238+
else:
239+
sp.check_call('lcov --add-tracefile "%s" --output-file "%s"'
240+
% (tracefile, options.final_tracefile),
241+
shell=True, stdout=output_file, stderr=output_file)
242+
print("### done.", file=output_file, flush=True)
243+
output_file.close()
244+
245+
246+
def generate_html():
247+
logging.info("Generating HTML report ...")
248+
output_file = open('%s/generate_html.log' % (options.coverage_dir,), mode='a')
249+
sp.check_call('genhtml --output-directory %s --demangle-cpp --num-spaces 2 --sort '
250+
'--title "PFASST++ Test Coverage" --prefix "%s" --function-coverage --legend "%s"'
251+
% (options.coverage_dir, options.base_dir, options.final_tracefile),
252+
shell=True, stdout=output_file, stderr=output_file)
253+
output_file.close()
254+
logging.info("Coverage report can be found in: file://%s/index.html" % options.coverage_dir)
255+
256+
257+
if __name__ == "__main__":
258+
if not is_lcov_available():
259+
raise RuntimeError("Required commands could not be found.")
260+
setup_and_init_options()
261+
get_test_directories()
262+
run_tests()
263+
aggregate_tracefiles()
264+
generate_html()

0 commit comments

Comments
 (0)