[Coverage] Use gcovr for coverage analysis, instead of unstable lcov
authorDongju Chae <dongju.chae@samsung.com>
Mon, 20 Apr 2020 11:39:02 +0000 (20:39 +0900)
committer송욱/On-Device Lab(SR)/Staff Engineer/삼성전자 <wook16.song@samsung.com>
Tue, 21 Apr 2020 06:18:59 +0000 (15:18 +0900)
This patch uses gcovr for coverage analysis, instead of unstable lcov.

Signed-off-by: Dongju Chae <dongju.chae@samsung.com>
README.md
packaging/npu-engine.spec
tools/unittestcoverage.py [deleted file]

index 7623593..355ab74 100644 (file)
--- a/README.md
+++ b/README.md
@@ -15,7 +15,6 @@ NPU_SystemService/    Root directory
 |   |-- unittests     Source files for Unit Tests
 |   |-- apptests      Source files for App Tests
 |   `-- utils         Source files for Test Utils
-|-- tools/            Useful tools or scripts
 |-- packaging/        Tizen Packaging Files (i.e., FastModel Simulator)
 |-- debian/           Debian Packaging Files (i.e., Ubuntu 16.04)
 `-- doc/              Documentation
index c5cb986..57faa8d 100644 (file)
@@ -27,8 +27,7 @@ BuildRequires:        gtest-devel
 
 # test coverage dependency
 %if 0%{?test_coverage}
-BuildRequires: lcov
-BuildRequires: python
+BuildRequires: gcovr
 %endif
 
 # NPU emulation dependency (used in unittests only)
@@ -64,8 +63,10 @@ cp %{SOURCE1001} .
 # @todo (NYI) build three times for the three different transport plugins for npu-engine
 
 %if 0%{?test_coverage}
-CXXFLAGS="${CXXFLAGS} -fprofile-arcs -ftest-coverage"
-CFLAGS="${CFLAGS} -fprofile-arcs -ftest-coverage"
+# Disable Optimizations (should unset _FORTIFY_SOURCE to use -O0)
+%define COVERAGE_FLAGS --coverage -Wp,-U_FORTIFY_SOURCE -Wp,-D_FORTIFY_SOURCE=0 -O0
+CXXFLAGS="${CXXFLAGS} %{COVERAGE_FLAGS}"
+CFLAGS="${CFLAGS} %{COVERAGE_FLAGS}"
 %endif
 
 meson build --prefix=%{_prefix} --sysconfdir=%{_sysconfdir} --libdir=%{_libdir} --includedir=%{_includedir} --bindir=%{neexampledir} -Denable_npu_emul=%{enable_npu_emul} -Denable_data_manip=true
@@ -107,24 +108,19 @@ DESTDIR=%{buildroot} ninja install -C build %{?_smp_mflags}
   # 'lcov' generates the date format with UTC time zone by default. Let's replace UTC with KST
   TZ='Asia/Seoul'; export TZ
 
-  $(pwd)/tools/unittestcoverage.py module $(pwd)/src
+  pushd build
 
-  # Get commit info
-  VCS=`cat ${RPM_SOURCE_DIR}/npu-engine.spec | grep "^VCS:" | sed "s|VCS:\\W*\\(.*\\)|\\1|"`
+  # Collect coverage statistics using gcovr
+  gcovr -r .. --html --html-details \
+    --html-title "NPU SystemService Coverage" \
+    --filter ../src/ --filter ../include/ \
+    -o index.html
 
-  # Create human readable unit test coverate report web page
-  # Create null gcda files if gcov didn't create it
-  find . -name "*.gcno" -exec sh -c 'touch -a "${1%.gcno}.gcda"' _ {} \;
-  # Generate report
-  lcov -t 'NPU SystemService Unit Test Coverage' -o unittest.info -c -d . -b $(pwd)/src --no-external
-  # Exclude generated files
-  lcov -r unittest.info "*/tools/*" "*/tests/*" "*/meson*/*" -o unittest-filtered.info
-  # Visualize the report
-  genhtml -o result unittest-filtered.info -t "NPU SystemService %{version}-%{release} ${VCS}" --ignore-errors source -p ${RPM_BUILD_DIR}
-
-  # Copy unittest results
+  # Copy the coverage results
   mkdir -p %{buildroot}%{_datadir}/unittest/
-  cp -r result %{buildroot}%{_datadir}/unittest/
+  mv *.html %{buildroot}%{_datadir}/unittest/
+
+  popd
 %endif
 
 %files
diff --git a/tools/unittestcoverage.py b/tools/unittestcoverage.py
deleted file mode 100755 (executable)
index 502d78f..0000000
+++ /dev/null
@@ -1,331 +0,0 @@
-#!/usr/bin/env python
-
-##
-# Copyright (c) 2018 Samsung Electronics Co., Ltd. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#     http://www.apache.org/licenses/LICENSE-2.0
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-##
-# @file unittestcoverage.py
-# @brief Calculate and show unit test coverate rate.
-# @author MyungJoo Ham <myungjoo.ham@samsung.com>
-# @note
-# Precondition:
-#  The user must have executed cmake/make build for all compoennts with -fprofile-arcs -ftest-coverage enabled
-#  All the unit tests binaries should have been executed.
-#  Other than the unit test binaries, no other built binaries should be executed, yet
-#
-# Usage: (for the case of NPU_SystemService.git)
-#
-#  $ python unittestcoverage.py module $(PWD)/core
-#  Please use absolute path to the module directory.
-#
-# Limitation of this version: supports c/c++ only (.c, .cpp, .h, .hpp)
-
-from __future__ import print_function
-import re
-import os
-import os.path
-import sys
-
-debugprint = 0
-
-## @brief Debug Print
-#
-# @param str The string to be debug-printed
-def dprint(str):
-  global debugprint
-  if debugprint == 1:
-    print(str)
-
-## @brief Search for c/c++ files not being detected by gcov
-#
-# @param gcovOutput output of gcov
-# @param path Path to be audited
-def auditEvaders(gcovOutput, path):
-  out = gcovOutput
-
-  targetFiles = {}
-  # Generate target file lists
-  dprint("Walking in " + path)
-  for root, dirs, files in os.walk(path):
-    for file in files:
-      # TODO 1 : Support other than C/C++
-      # TODO 2 : case insensitive
-      if file.endswith(".cpp") or file.endswith(".c") or \
-         file.endswith(".h") or file.endswith(".hpp"):
-        dprint(file)
-
-        # exclude unittest itself
-        if (re.match("^unittest\/", root[len(path)+1:])):
-          continue
-        # exclude files from build directory (auto generated)
-        if (re.match("^build\/", root[len(path)+1:])):
-          continue
-        # exclude CMake artifacts
-        if file.startswith("CMakeCCompilerId") or file.startswith("CMakeCXXCompilerId"):
-          continue
-
-        # (-1, -1) means untracked file
-        targetFiles[os.path.join(root, file)[len(path)+1:]] = (-1, -1)
-        dprint("Registered: " + os.path.join(root, file)[len(path)+1:])
-
-
-  # From the begging, read each line and process "targetFiles"
-  parserStatus = 0 # Nothing / Completed Session
-  parsingForFile = ""
-  lastlines = 0
-  for line in out.splitlines():
-    m = re.match("File '(.+)'$", line)
-    if m:
-      if parserStatus == 1:
-        sys.exit("[CRITIAL BUG] Status Mismatch: need to be 0")
-
-      parsingForFile = m.group(1)
-      if parsingForFile not in targetFiles:
-        if re.match("^CMakeCCompilerId", parsingForFile): # ignore cmake artifacts
-          continue
-        if re.match("^CMakeCXXCompilerId", parsingForFile): # ignore cmake artifacts
-          continue
-        print("[CRITICAL BUG] Hey! File " + parsingForFile + " is not being found?")
-        targetFiles[parsingForFile] = (-1, -1)
-      elif targetFiles[parsingForFile] == (-1, -1):
-        dprint("Matching new file: " + parsingForFile)
-      else:
-        dprint("Duplicated file: " + parsingForFile)
-
-      parserStatus = 1 # File name parsed
-      continue
-
-    m = re.match("Lines executed:(\d+.\d+)% of (\d+)$", line)
-    if m:
-      if parserStatus == 0:
-        continue
-      if parserStatus == 2:
-        sys.exit("[CRITICAL BUG] Status Mismatch: need to be 1")
-      parserStatus = 2
-
-      rate = float(m.group(1))
-      lines = int(m.group(2))
-
-      if parsingForFile not in targetFiles:
-        sys.exit("[CRITICAL BUG] targetFiles broken: not found: " + parsingForFile)
-      (oldrate, oldlines) = targetFiles[parsingForFile]
-
-      if oldlines == -1: # new instancfe
-        targetFiles[parsingForFile] = (rate, lines)
-      elif lines == oldlines and rate > oldrate: # overwrite
-        targetFiles[parsingForFile] = (rate, lines)
-        # anyway, in this mechanis, this can't happen
-        sys.exit("[CRITICAL BUG] file " + parsingForFile + " occurs twice??? case 1")
-      else:
-        sys.exit("[CRITICAL BUG] file " + parsingForFile + " occurs twice??? case 2")
-      continue
-
-    if re.match("Creating '", line):
-      if parserStatus == 1:
-        sys.exit("[CRITICAL BUG] Status mismatch. It should be 0 or 2!")
-      parserStatus = 0
-      continue
-
-    if re.match("^\s*$", line):
-      continue
-
-    sys.exit("[CRITICAL BUG] incorrect gcov output: " + line)
-
-  totalTestedLine = 0
-  totalAllLine = 0
-
-  # For each "targetFiles", check if they are covered.
-  for filename, (rate, lines) in targetFiles.iteritems():
-    if lines == -1: # untracked file
-      # CAUTION! wc does line count of untracked files. it counts lines differently
-      # TODO: Count lines with the policy of gcov
-      linecount = os.popen("wc -l " + os.path.join(path, filename)).read()
-      m = re.match("^(\d+)", linecount)
-      if not m:
-        sys.exit("Cannot read proper wc results for " + filename)
-      lines = int(m.group(1))
-      rate = 0.0
-      print("Untracked File Found!!!")
-      print("[" + filename + "] : 0% of " + m.group(1) + " lines")
-
-    totalAllLine += lines
-    totalTestedLine += int((lines * rate / 100.0) + 0.5)
-
-  rate = 100.0 * totalTestedLine / totalAllLine
-  print("=======================================================")
-  print("Lines: " + str(totalAllLine) + "  Covered Rate: " + str(rate) + "%")
-  print("=======================================================")
-
-## @brief Do the check for unit test coverage on the given path
-#
-# @param path The path to be audited
-# @return (number of lines counted, ratio of unittested lines)
-def check_component(path):
-  # Remove last trailing /
-  if path[-1:] == '/':
-    path = path[:-1]
-
-  buildpath = os.path.join(path, "build")
-  searchlimit = 5
-  buildpathconst = path
-
-  # If path/build does not exist, try path/../build, path/../../build, ... (limit = 5)
-  while ((not os.path.isdir(buildpath)) and searchlimit > 0):
-    searchlimit = searchlimit - 1
-    buildpathconst = os.path.join(buildpathconst, "..")
-    buildpath = os.path.join(buildpathconst, "build")
-
-  # Get gcov report from unittests
-  out = os.popen("gcov -p -r -s " + path + " `find " + buildpath +
-                 " -name *.gcno`").read()
-  dprint(out)
-
-  total_lines = 0
-  total_covered = 0
-  total_rate = 0.0
-  # Calculate a line coverage per file
-  for each_line in out.splitlines():
-    m = re.match("Lines executed:(\d+.\d+)% of (\d+)$", each_line)
-    if m:
-      rate = float(m.group(1))
-      lines = int(m.group(2))
-
-      total_lines = total_lines + lines
-      total_covered = total_covered + (rate * lines)
-
-  if total_lines > 0:
-    total_rate = total_covered / total_lines
-
-  return (total_lines, total_rate)
-  # Call auditEvaders(out, path) if we really become paranoid.
-
-## @brief Check unit test coverage for a specific path. (every code in that path, recursively)
-#
-# @param The audited path.
-def cmd_module(paths):
-  lines = 0
-  rate = 0
-  countrated = 0
-
-  for path in paths:
-    (l, rate) = check_component(path)
-    lines = lines + l
-    countrated = countrated + (rate * l)
-
-  rate = countrated / lines
-  if lines < 0:
-    return -1
-
-  print("\n\n===========================================================")
-  print("Paths for test coverage " + str(paths))
-  print("%d Lines with %0.2f%% unit test coverage" % (lines, rate))
-  print("===========================================================\n\n\n")
-  return 0
-
-countLines = 0
-countCoveredLines = 0
-
-## @brief Search for directories containing CMakeLists.txt
-#
-# @param path The search target
-def analyzeEveryFirstCMakeListsTxt(path):
-  global countLines, countCoveredLines
-  targetName = os.path.join(path, "CMakeLists.txt")
-  targetDir = os.path.join(path, "build")
-
-  if os.path.isfile(targetName):
-    if os.path.isdir(targetDir):
-      (lines, rate) = check_component(path)
-      coveredLines = int((rate * float(lines) + 0.5) / 100.0)
-      countLines = countLines + lines
-      countCoveredLines = countCoveredLines + coveredLines
-      print("[ROS Component]" + str(path) + ": " + str(lines) + " Lines with " + str(rate) + "% unit test coverage")
-      return 0
-    print("[Warning] " + str(path) + " has CMakeLists.txt but not build directory. This may occur if you build with app option")
-    return 0
-
-  filenames = os.listdir(path)
-  for filename in filenames:
-    fullname = os.path.join(path, filename)
-    if (os.path.isdir(fullname)):
-      analyzeEveryFirstCMakeListsTxt(fullname)
-  return 0
-
-## @brief Check all subdirectories with CMakeLists.txt and thier children, skipping subdirectories without it.
-#
-# @path The search target
-def cmd_all(path):
-  analyzeEveryFirstCMakeListsTxt(path)
-  print("\n\n===========================================================")
-  print("Total Lines = " + str(countLines) + " / Covered Lines = " + str(countCoveredLines) + " ( " + str(100.0 * countCoveredLines / countLines) + "% )")
-  print("===========================================================\n\n\n")
-  return 0
-
-help_messages = {
-  'all':
-    'python unittestcoverage.py all [PATH to the Audri ROS directory] {additional options}\n'
-    '',
-  'module':
-    'python unittestcoverage.py module [PATH to the component] {additional options}\n'
-    '',
-  'help':
-    'python unittestcoverage.py [command] [command specific options]\n'
-    '\n'
-    'Comamnds:\n'
-    '    all\n'
-    '    module\n'
-    '    help\n'
-    '\n'
-    'Additional Options:\n'
-    '    -d enable debugprint\n'
-    '\n',
-}
-
-## @brief Shows the help message
-#
-# @param command the command line argument
-def cmd_help(command=None):
-  if (command is None) or (not command):
-    command = 'help'
-  print(help_messages[command])
-  return 0
-
-## @brief The main function
-#
-def main():
-  num = len(sys.argv)
-  if num < 2:
-    return cmd_help()
-
-  cmd = sys.argv[1]
-  args = []
-
-  for arg in sys.argv[2:]:
-    if arg == '-d':
-      global debugprint
-      debugprint = 1
-    else:
-      args.append(arg)
-
-  if cmd == 'help':
-    arg = (sys.argv[2] if num > 2 else None)
-    return cmd_help(arg)
-  elif cmd == 'all':
-    return cmd_all(args)
-  elif cmd == 'module':
-    return cmd_module(args)
-
-  return cmd_help()
-
-sys.exit(main())