Skip to content

Commit fc820f5

Browse files
authored
Merge pull request #8154 from lorjala/unittests-coveragefiltering
Unit tests: add code coverage filtering
2 parents 815683c + 00397b2 commit fc820f5

File tree

5 files changed

+182
-86
lines changed

5 files changed

+182
-86
lines changed

UNITTESTS/mbed_unittest.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
from __future__ import print_function
2424
import os
2525
import logging
26+
import re
2627

2728
from unit_test.options import get_options_parser, \
2829
pretty_print_test_options
2930
from unit_test.settings import DEFAULT_CMAKE_GENERATORS
3031
from unit_test.test import UnitTestTool
3132
from unit_test.new import UnitTestGeneratorTool
33+
from unit_test.coverage import CoverageAPI
3234

3335
def _mbed_unittest_test(options, cwd, pwd):
3436
if options is None:
@@ -80,14 +82,28 @@ def _mbed_unittest_test(options, cwd, pwd):
8082
tool.build_tests()
8183

8284
if options.run_only:
85+
tool.run_tests(filter_regex=options.test_regex)
86+
8387
# If code coverage generation:
8488
if options.coverage:
85-
# Run tests and generate reports
86-
tool.generate_coverage_report(coverage_output_type=options.coverage,
87-
excludes=[pwd, options.build],
88-
build_path=options.build)
89-
else:
90-
tool.run_tests(filter_regex=options.test_regex) # Only run tests
89+
cov_api = CoverageAPI(
90+
mbed_os_root=os.path.normpath(os.path.join(pwd, "..")),
91+
build_dir=options.build)
92+
93+
# Generate reports
94+
outputs = [options.coverage]
95+
if options.coverage == "both":
96+
outputs = ["html", "xml"]
97+
98+
excludes = [pwd, options.build]
99+
100+
if not options.include_headers:
101+
excludes.append(re.compile(".*\\.h"))
102+
103+
cov_api.generate_reports(outputs=outputs,
104+
excludes=excludes,
105+
filter_regex=options.test_regex,
106+
build_path=options.build)
91107

92108
def _mbed_unittest_new(options, pwd):
93109
if options is None:

UNITTESTS/unit_test/coverage.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""
2+
Copyright (c) 2018, Arm Limited
3+
SPDX-License-Identifier: Apache-2.0
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
17+
18+
GENERATE TEST CODE COVERAGE
19+
"""
20+
21+
import os
22+
import logging
23+
import posixpath
24+
import re
25+
26+
from .utils import execute_program
27+
from .get_tools import get_gcov_program, \
28+
get_gcovr_program
29+
from .settings import COVERAGE_OUTPUT_TYPES
30+
31+
32+
class CoverageAPI(object):
33+
"""
34+
Generate code coverage reports for unit tests.
35+
"""
36+
37+
def __init__(self, mbed_os_root=None, build_dir=None):
38+
self.root = mbed_os_root
39+
40+
if not self.root:
41+
self.root = os.path.normpath(os.path.join(
42+
os.path.dirname(os.path.realpath(__file__)),
43+
"../.."))
44+
45+
self.build_dir = build_dir
46+
47+
if not self.build_dir:
48+
logging.error("No build directory given for CoverageAPI.")
49+
50+
def _gen_cmd(self, coverage_type, excludes, filter_regex):
51+
# Generate coverage generation command:
52+
args = [get_gcovr_program(),
53+
"--gcov-executable",
54+
get_gcov_program(),
55+
"-r",
56+
os.path.relpath(self.root, self.build_dir),
57+
"."]
58+
59+
if coverage_type == "html":
60+
args.extend(["--html",
61+
"--html-detail",
62+
"-o",
63+
"./coverage/index.html"])
64+
elif coverage_type == "xml":
65+
args.extend(["-x",
66+
"-o",
67+
"./coverage.xml"])
68+
else:
69+
logging.error("Invalid coverage output type: %s" % coverage_type)
70+
71+
# Add exclude filters:
72+
for path in excludes:
73+
# Use posix separators if path is string
74+
if isinstance(path, ("".__class__, u"".__class__)):
75+
path = path.replace("\\", "/")
76+
args.extend(["-e", path])
77+
# Use regular expressions as is
78+
elif isinstance(path, type(re.compile(""))):
79+
args.extend(["-e", path.pattern])
80+
81+
# Add include filters:
82+
if filter_regex:
83+
filters = filter_regex.split(",")
84+
85+
for filt in filters:
86+
regex = "(.+/)?%s" % filt.replace("-", "/")
87+
args.extend(["-f", regex])
88+
89+
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
90+
args.append("-v")
91+
92+
return args
93+
94+
def generate_reports(self,
95+
outputs,
96+
excludes=None,
97+
filter_regex=None,
98+
build_path=None):
99+
"""
100+
Run tests to generate coverage data, and generate coverage reports.
101+
102+
Positional arguments:
103+
outputs - list of types to generate
104+
105+
Keyword arguments:
106+
excludes - list of paths to exclude from the report
107+
filter_regex - comma-separated string to use for test filtering
108+
build_path - build path
109+
"""
110+
111+
# Check for the tool
112+
if get_gcovr_program() is None:
113+
logging.error(
114+
"No gcovr tool found in path. " +
115+
"Cannot generate coverage reports.")
116+
return
117+
118+
if build_path is None:
119+
build_path = os.getcwd()
120+
121+
if outputs is None:
122+
outputs = []
123+
124+
if excludes is None:
125+
excludes = []
126+
127+
for output in outputs:
128+
# Skip if invalid/unsupported output type
129+
if output not in COVERAGE_OUTPUT_TYPES:
130+
logging.warning(
131+
"Invalid output type. " +
132+
"Skip coverage report for type: %s." % output.upper())
133+
continue
134+
135+
if output == "html":
136+
# Create a build directory if not exist
137+
coverage_path = os.path.join(build_path, "coverage")
138+
if not os.path.exists(coverage_path):
139+
os.mkdir(coverage_path)
140+
141+
# Generate the command
142+
args = self._gen_cmd(output, excludes, filter_regex)
143+
144+
# Run the coverage tool
145+
execute_program(
146+
args,
147+
"%s code coverage report generation failed." % output.upper(),
148+
"%s code coverage report created." % output.upper())

UNITTESTS/unit_test/options.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import argparse
2222
import logging
2323

24-
from .settings import CMAKE_GENERATORS, MAKE_PROGRAMS, COVERAGE_TYPES
24+
from .settings import CMAKE_GENERATORS, MAKE_PROGRAMS, COVERAGE_ARGS
2525
from .get_tools import get_make_tool
2626

2727
def get_options_parser():
@@ -71,10 +71,15 @@ def get_options_parser():
7171
dest="debug_build")
7272

7373
parser.add_argument("--coverage",
74-
choices=COVERAGE_TYPES,
74+
choices=COVERAGE_ARGS,
7575
help="Generate code coverage report",
7676
dest="coverage")
7777

78+
parser.add_argument("--include-headers",
79+
action="store_true",
80+
help="Include headers to coverage reports, defaults to false.",
81+
dest="include_headers")
82+
7883
parser.add_argument("-m",
7984
"--make-program",
8085
default=get_make_tool(),
@@ -140,3 +145,4 @@ def pretty_print_test_options(options=None):
140145
if options.coverage:
141146
logging.info(" [%s] \tGenerate coverage reports", "SET")
142147
logging.info(" \t\t - Output: %s", options.coverage)
148+
logging.info(" \t\t - Include headers: %s", options.include_headers)

UNITTESTS/unit_test/settings.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
"ninja": "Ninja"
3030
}
3131

32-
COVERAGE_TYPES = ["html",
32+
COVERAGE_ARGS = ["html",
3333
"xml",
3434
"both"]
3535

36+
COVERAGE_OUTPUT_TYPES = ["html", "xml"]
37+
3638
CXX_COMPILERS = ["g++-6", "g++-8", "g++-7", "g++-5", "g++-4.9", "g++"]
3739

3840
C_COMPILERS = ["gcc-6", "gcc-8", "gcc-7", "gcc-5", "gcc-4.9", "gcc"]

UNITTESTS/unit_test/test.py

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@
2727
from .get_tools import get_make_tool, \
2828
get_cmake_tool, \
2929
get_cxx_tool, \
30-
get_c_tool, \
31-
get_gcov_program, \
32-
get_gcovr_program
30+
get_c_tool
3331
from .settings import DEFAULT_CMAKE_GENERATORS
3432

3533
class UnitTestTool(object):
@@ -115,80 +113,6 @@ def build_tests(self):
115113
"Building unit tests failed.",
116114
"Unit tests built successfully.")
117115

118-
def _get_coverage_script(self, coverage_type, excludes):
119-
args = [get_gcovr_program(),
120-
"--gcov-executable",
121-
get_gcov_program(),
122-
"-r",
123-
"../..",
124-
"."]
125-
126-
if coverage_type == "html":
127-
args.extend(["--html",
128-
"--html-detail",
129-
"-o",
130-
"./coverage/index.html"])
131-
elif coverage_type == "xml":
132-
args.extend(["-x",
133-
"-o",
134-
"./coverage.xml"])
135-
136-
for path in excludes:
137-
args.extend(["-e", path.replace("\\", "/")])
138-
139-
#Exclude header files from report
140-
args.extend(["-e", ".*\.h"])
141-
142-
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
143-
args.append("-v")
144-
145-
return args
146-
147-
def generate_coverage_report(self,
148-
coverage_output_type=None,
149-
excludes=None,
150-
build_path=None):
151-
"""
152-
Run tests to generate coverage data, and generate coverage reports.
153-
"""
154-
155-
self.run_tests()
156-
157-
if get_gcovr_program() is None:
158-
logging.error("No gcovr tool found in path. \
159-
Cannot generate coverage report.")
160-
return
161-
162-
if build_path is None:
163-
build_path = os.getcwd()
164-
165-
if coverage_output_type is None:
166-
logging.warning("No coverage output type give. \
167-
Cannot generate coverage reports.")
168-
return
169-
170-
if excludes is None:
171-
excludes = []
172-
173-
if coverage_output_type == "html" or coverage_output_type == "both":
174-
# Create build directory if not exist.
175-
coverage_path = os.path.join(build_path, "coverage")
176-
if not os.path.exists(coverage_path):
177-
os.mkdir(coverage_path)
178-
179-
args = self._get_coverage_script("html", excludes)
180-
181-
execute_program(args,
182-
"HTML code coverage report generation failed.",
183-
"HTML code coverage report created.")
184-
185-
if coverage_output_type == "xml" or coverage_output_type == "both":
186-
args = self._get_coverage_script("xml", excludes)
187-
188-
execute_program(args,
189-
"XML code coverage report generation failed.",
190-
"XML code coverage report created.")
191-
192116
def run_tests(self, filter_regex=None):
193117
"""
194118
Run unit tests.

0 commit comments

Comments
 (0)