4 # Copyright (c) 2018 Samsung Electronics Co., Ltd. All Rights Reserved.
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
18 # @file unittestcoverage.py
19 # @brief Calculate and show unit test coverate rate.
20 # @author MyungJoo Ham <myungjoo.ham@samsung.com>
23 # The user must have executed cmake/make build for all compoennts with -fprofile-arcs -ftest-coverage enabled
24 # All the unit tests binaries should have been executed.
25 # Other than the unit test binaries, no other built binaries should be executed, yet
27 # Usage: (for the case of STAR/AuDri.git)
29 # $ unittestcoverage module /home/abuild/rpmbuild/BUILD/audri-1.1.1/ROS/autodrive/
30 # Please use absolute path to the module directory.
32 # $ unittestcoverage all /home/abuild/rpmbuild/BUILD/audri-1.1.1/ROS/
33 # Please use absolute path to the ROS module root dir
35 # Limitation of this version: supports c/c++ only (.c, .cc, .h, .hpp)
38 from __future__ import print_function
48 # @param str The string to be debug-printed
54 ## @brief Search for c/c++ files not being detected by gcov
56 # @param gcovOutput output of gcov
57 # @param path Path to be audited
58 def auditEvaders(gcovOutput, path):
62 # Generate target file lists
63 dprint("Walking in " + path)
64 for root, dirs, files in os.walk(path):
66 # TODO 1 : Support other than C/C++
67 # TODO 2 : case insensitive
68 if file.endswith(".cc") or file.endswith(".c") or \
69 file.endswith(".h") or file.endswith(".hpp"):
72 # exclude unittest itself
73 if (re.match("^unittest\/", root[len(path)+1:])):
75 # exclude files from build directory (auto generated)
76 if (re.match("^build\/", root[len(path)+1:])):
78 # exclude CMake artifacts
79 if file.startswith("CMakeCCompilerId") or file.startswith("CMakeCXXCompilerId"):
82 # (-1, -1) means untracked file
83 targetFiles[os.path.join(root, file)[len(path)+1:]] = (-1, -1)
84 dprint("Registered: " + os.path.join(root, file)[len(path)+1:])
87 # From the begging, read each line and process "targetFiles"
88 parserStatus = 0 # Nothing / Completed Session
91 for line in out.splitlines():
92 m = re.match("File '(.+)'$", line)
95 sys.exit("[CRITIAL BUG] Status Mismatch: need to be 0")
97 parsingForFile = m.group(1)
98 if parsingForFile not in targetFiles:
99 if re.match("^CMakeCCompilerId", parsingForFile): # ignore cmake artifacts
101 if re.match("^CMakeCXXCompilerId", parsingForFile): # ignore cmake artifacts
103 print("[CRITICAL BUG] Hey! File " + parsingForFile + " is not being found?")
104 targetFiles[parsingForFile] = (-1, -1)
105 elif targetFiles[parsingForFile] == (-1, -1):
106 dprint("Matching new file: " + parsingForFile)
108 dprint("Duplicated file: " + parsingForFile)
110 parserStatus = 1 # File name parsed
113 m = re.match("Lines executed:(\d+.\d+)% of (\d+)$", line)
115 if parserStatus == 0:
117 if parserStatus == 2:
118 sys.exit("[CRITICAL BUG] Status Mismatch: need to be 1")
121 rate = float(m.group(1))
122 lines = int(m.group(2))
124 if parsingForFile not in targetFiles:
125 sys.exit("[CRITICAL BUG] targetFiles broken: not found: " + parsingForFile)
126 (oldrate, oldlines) = targetFiles[parsingForFile]
128 if oldlines == -1: # new instancfe
129 targetFiles[parsingForFile] = (rate, lines)
130 elif lines == oldlines and rate > oldrate: # overwrite
131 targetFiles[parsingForFile] = (rate, lines)
132 # anyway, in this mechanis, this can't happen
133 sys.exit("[CRITICAL BUG] file " + parsingForFile + " occurs twice??? case 1")
135 sys.exit("[CRITICAL BUG] file " + parsingForFile + " occurs twice??? case 2")
138 if re.match("Creating '", line):
139 if parserStatus == 1:
140 sys.exit("[CRITICAL BUG] Status mismatch. It should be 0 or 2!")
144 if re.match("^\s*$", line):
147 sys.exit("[CRITICAL BUG] incorrect gcov output: " + line)
152 # For each "targetFiles", check if they are covered.
153 for filename, (rate, lines) in targetFiles.iteritems():
154 if lines == -1: # untracked file
155 # CAUTION! wc does line count of untracked files. it counts lines differently
156 # TODO: Count lines with the policy of gcov
157 linecount = os.popen("wc -l " + os.path.join(path, filename)).read()
158 m = re.match("^(\d+)", linecount)
160 sys.exit("Cannot read proper wc results for " + filename)
161 lines = int(m.group(1))
163 print("Untracked File Found!!!")
164 print("[" + filename + "] : 0% of " + m.group(1) + " lines")
166 totalAllLine += lines
167 totalTestedLine += int((lines * rate / 100.0) + 0.5)
169 rate = 100.0 * totalTestedLine / totalAllLine
170 print("=======================================================")
171 print("Lines: " + str(totalAllLine) + " Covered Rate: " + str(rate) + "%")
172 print("=======================================================")
174 ## @brief Do the check for unit test coverage on the given path
176 # @param path The path to be audited
177 # @return (number of lines counted, ratio of unittested lines)
178 def check_component(path):
179 # Remove last trailing /
183 buildpath = os.path.join(path, "build")
185 buildpathconst = path
187 # If path/build does not exist, try path/../build, path/../../build, ... (limit = 5)
188 while ((not os.path.isdir(buildpath)) and searchlimit > 0):
189 searchlimit = searchlimit - 1
190 buildpathconst = os.path.join(buildpathconst, "..")
191 buildpath = os.path.join(buildpathconst, "build")
193 # Get gcov report from unittests
194 out = os.popen("gcov -p -r -s " + path + " `find " + buildpath +
195 " -name *.gcno`").read()
201 # Calculate a line coverage per file
202 for each_line in out.splitlines():
203 m = re.match("Lines executed:(\d+.\d+)% of (\d+)$", each_line)
205 rate = float(m.group(1))
206 lines = int(m.group(2))
208 total_lines = total_lines + lines
209 total_covered = total_covered + (rate * lines)
212 total_rate = total_covered / total_lines
214 return (total_lines, total_rate)
215 # Call auditEvaders(out, path) if we really become paranoid.
217 ## @brief Check unit test coverage for a specific path. (every code in that path, recursively)
219 # @param The audited path.
220 def cmd_module(paths):
226 (l, rate) = check_component(path)
228 countrated = countrated + (rate * l)
230 rate = countrated / lines
234 print("\n\n===========================================================")
235 print("Paths for test coverage " + str(paths))
236 print("%d Lines with %0.2f%% unit test coverage" % (lines, rate))
237 print("===========================================================\n\n\n")
241 countCoveredLines = 0
243 ## @brief Search for directories containing CMakeLists.txt
245 # @param path The search target
246 def analyzeEveryFirstCMakeListsTxt(path):
247 global countLines, countCoveredLines
248 targetName = os.path.join(path, "CMakeLists.txt")
249 targetDir = os.path.join(path, "build")
251 if os.path.isfile(targetName):
252 if os.path.isdir(targetDir):
253 (lines, rate) = check_component(path)
254 coveredLines = int((rate * float(lines) + 0.5) / 100.0)
255 countLines = countLines + lines
256 countCoveredLines = countCoveredLines + coveredLines
257 print("[ROS Component]" + str(path) + ": " + str(lines) + " Lines with " + str(rate) + "% unit test coverage")
259 print("[Warning] " + str(path) + " has CMakeLists.txt but not build directory. This may occur if you build with app option")
262 filenames = os.listdir(path)
263 for filename in filenames:
264 fullname = os.path.join(path, filename)
265 if (os.path.isdir(fullname)):
266 analyzeEveryFirstCMakeListsTxt(fullname)
269 ## @brief Check all subdirectories with CMakeLists.txt and thier children, skipping subdirectories without it.
271 # @path The search target
273 analyzeEveryFirstCMakeListsTxt(path)
274 print("\n\n===========================================================")
275 print("Total Lines = " + str(countLines) + " / Covered Lines = " + str(countCoveredLines) + " ( " + str(100.0 * countCoveredLines / countLines) + "% )")
276 print("===========================================================\n\n\n")
281 'python unittestcoverage.py all [PATH to the Audri ROS directory] {additional options}\n'
284 'python unittestcoverage.py module [PATH to the component] {additional options}\n'
287 'python unittestcoverage.py [command] [command specific options]\n'
294 'Additional Options:\n'
295 ' -d enable debugprint\n'
299 ## @brief Shows the help message
301 # @param command the command line argument
302 def cmd_help(command=None):
303 if (command is None) or (not command):
305 print(help_messages[command])
308 ## @brief The main function
318 for arg in sys.argv[2:]:
326 arg = (sys.argv[2] if num > 2 else None)
330 elif cmd == 'module':
331 return cmd_module(args)