[Coverity] Fix coverity issues
[platform/core/ml/nntrainer.git] / test / unittestcoverage.py
1 #!/usr/bin/env python3
2
3 ##
4 # Copyright (c) 2018 Samsung Electronics Co., Ltd. All Rights Reserved.
5 #
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.
15 #
16
17 ##
18 # @file unittestcoverage.py
19 # @brief Calculate and show unit test coverate rate.
20 # @author MyungJoo Ham <myungjoo.ham@samsung.com>
21 # @note
22 # Precondition:
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
26 #
27 # Usage: (for the case of STAR/AuDri.git)
28 #
29 #  $ unittestcoverage module /home/abuild/rpmbuild/BUILD/audri-1.1.1/ROS/autodrive/
30 #  Please use absolute path to the module directory.
31 #
32 #  $ unittestcoverage all /home/abuild/rpmbuild/BUILD/audri-1.1.1/ROS/
33 #  Please use absolute path to the ROS module root dir
34 #
35 # Limitation of this version: supports c/c++ only (.c, .cc, .h, .hpp)
36 #
37
38 from __future__ import print_function
39 import re
40 import os
41 import os.path
42 import sys
43
44 debugprint = 0
45
46 ## @brief Debug Print
47 #
48 # @param str The string to be debug-printed
49 def dprint(str):
50   global debugprint
51   if debugprint == 1:
52     print(str)
53
54 ## @brief Search for c/c++ files not being detected by gcov
55 #
56 # @param gcovOutput output of gcov
57 # @param path Path to be audited
58 def auditEvaders(gcovOutput, path):
59   out = gcovOutput
60
61   targetFiles = {}
62   # Generate target file lists
63   dprint("Walking in " + path)
64   for root, dirs, files in os.walk(path):
65     for file in files:
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"):
70         dprint(file)
71
72         # exclude unittest itself
73         if (re.match("^unittest\/", root[len(path)+1:])):
74           continue
75         # exclude files from build directory (auto generated)
76         if (re.match("^build\/", root[len(path)+1:])):
77           continue
78         # exclude CMake artifacts
79         if file.startswith("CMakeCCompilerId") or file.startswith("CMakeCXXCompilerId"):
80           continue
81
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:])
85
86
87   # From the begging, read each line and process "targetFiles"
88   parserStatus = 0 # Nothing / Completed Session
89   parsingForFile = ""
90   lastlines = 0
91   for line in out.splitlines():
92     m = re.match("File '(.+)'$", line)
93     if m:
94       if parserStatus == 1:
95         sys.exit("[CRITIAL BUG] Status Mismatch: need to be 0")
96
97       parsingForFile = m.group(1)
98       if parsingForFile not in targetFiles:
99         if re.match("^CMakeCCompilerId", parsingForFile): # ignore cmake artifacts
100           continue
101         if re.match("^CMakeCXXCompilerId", parsingForFile): # ignore cmake artifacts
102           continue
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)
107       else:
108         dprint("Duplicated file: " + parsingForFile)
109
110       parserStatus = 1 # File name parsed
111       continue
112
113     m = re.match("Lines executed:(\d+.\d+)% of (\d+)$", line)
114     if m:
115       if parserStatus == 0:
116         continue
117       if parserStatus == 2:
118         sys.exit("[CRITICAL BUG] Status Mismatch: need to be 1")
119       parserStatus = 2
120
121       rate = float(m.group(1))
122       lines = int(m.group(2))
123
124       if parsingForFile not in targetFiles:
125         sys.exit("[CRITICAL BUG] targetFiles broken: not found: " + parsingForFile)
126       (oldrate, oldlines) = targetFiles[parsingForFile]
127
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")
134       else:
135         sys.exit("[CRITICAL BUG] file " + parsingForFile + " occurs twice??? case 2")
136       continue
137
138     if re.match("Creating '", line):
139       if parserStatus == 1:
140         sys.exit("[CRITICAL BUG] Status mismatch. It should be 0 or 2!")
141       parserStatus = 0
142       continue
143
144     if re.match("^\s*$", line):
145       continue
146
147     sys.exit("[CRITICAL BUG] incorrect gcov output: " + line)
148
149   totalTestedLine = 0
150   totalAllLine = 0
151
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)
159       if not m:
160         sys.exit("Cannot read proper wc results for " + filename)
161       lines = int(m.group(1))
162       rate = 0.0
163       print("Untracked File Found!!!")
164       print("[" + filename + "] : 0% of " + m.group(1) + " lines")
165
166     totalAllLine += lines
167     totalTestedLine += int((lines * rate / 100.0) + 0.5)
168
169   rate = 100.0 * totalTestedLine / totalAllLine
170   print("=======================================================")
171   print("Lines: " + str(totalAllLine) + "  Covered Rate: " + str(rate) + "%")
172   print("=======================================================")
173
174 ## @brief Do the check for unit test coverage on the given path
175 #
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 /
180   if path[-1:] == '/':
181     path = path[:-1]
182
183   buildpath = os.path.join(path, "build")
184   searchlimit = 5
185   buildpathconst = path
186
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")
192
193   # Get gcov report from unittests
194   out = os.popen("gcov -p -r -s " + path + " `find " + buildpath +
195                  " -name *.gcno`").read()
196   dprint(out)
197
198   total_lines = 0
199   total_covered = 0
200   total_rate = 0.0
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)
204     if m:
205       rate = float(m.group(1))
206       lines = int(m.group(2))
207
208       total_lines = total_lines + lines
209       total_covered = total_covered + (rate * lines)
210
211   if total_lines > 0:
212     total_rate = total_covered / total_lines
213
214   return (total_lines, total_rate)
215   # Call auditEvaders(out, path) if we really become paranoid.
216
217 ## @brief Check unit test coverage for a specific path. (every code in that path, recursively)
218 #
219 # @param The audited path.
220 def cmd_module(paths):
221   lines = 0
222   rate = 0
223   countrated = 0
224
225   for path in paths:
226     (l, rate) = check_component(path)
227     lines = lines + l
228     countrated = countrated + (rate * l)
229
230   rate = countrated / lines
231   if lines < 0:
232     return -1
233
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")
238   return 0
239
240 countLines = 0
241 countCoveredLines = 0
242
243 ## @brief Search for directories containing CMakeLists.txt
244 #
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")
250
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")
258       return 0
259     print("[Warning] " + str(path) + " has CMakeLists.txt but not build directory. This may occur if you build with app option")
260     return 0
261
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)
267   return 0
268
269 ## @brief Check all subdirectories with CMakeLists.txt and thier children, skipping subdirectories without it.
270 #
271 # @path The search target
272 def cmd_all(path):
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")
277   return 0
278
279 help_messages = {
280   'all':
281     'python unittestcoverage.py all [PATH to the Audri ROS directory] {additional options}\n'
282     '',
283   'module':
284     'python unittestcoverage.py module [PATH to the component] {additional options}\n'
285     '',
286   'help':
287     'python unittestcoverage.py [command] [command specific options]\n'
288     '\n'
289     'Comamnds:\n'
290     '    all\n'
291     '    module\n'
292     '    help\n'
293     '\n'
294     'Additional Options:\n'
295     '    -d enable debugprint\n'
296     '\n',
297 }
298
299 ## @brief Shows the help message
300 #
301 # @param command the command line argument
302 def cmd_help(command=None):
303   if (command is None) or (not command):
304     command = 'help'
305   print(help_messages[command])
306   return 0
307
308 ## @brief The main function
309 #
310 def main():
311   num = len(sys.argv)
312   if num < 2:
313     return cmd_help()
314
315   cmd = sys.argv[1]
316   args = []
317
318   for arg in sys.argv[2:]:
319     if arg == '-d':
320       global debugprint
321       debugprint = 1
322     else:
323       args.append(arg)
324
325   if cmd == 'help':
326     arg = (sys.argv[2] if num > 2 else None)
327     return cmd_help(arg)
328   elif cmd == 'all':
329     return cmd_all(args)
330   elif cmd == 'module':
331     return cmd_module(args)
332
333   return cmd_help()
334
335 sys.exit(main())