build+ci: add code coverage support

This commit also adds GlobalConfigurationFixture to ensure that running
unit tests doesn't interact with system configuration (e.g., doesn't use
or alter user certificates).

Change-Id: I621ce14425a777c94d41b8132cd2858d91235b4d
Refs: #3808
diff --git a/.jenkins.d/10-build.sh b/.jenkins.d/10-build.sh
index 7f885fb..f0df3eb 100755
--- a/.jenkins.d/10-build.sh
+++ b/.jenkins.d/10-build.sh
@@ -27,7 +27,7 @@
 
 # Configure/build in debug mode with tests
 if [[ $JOB_NAME == *"code-coverage" ]]; then
-    COVERAGE="" # TODO add code coverage support
+    COVERAGE="--with-coverage"
 elif ! has OSX-10.9 $NODE_LABELS && ! has OSX-10.11 $NODE_LABELS; then
     ASAN="--with-sanitizer=address"
 fi
diff --git a/.jenkins.d/30-coverage.sh b/.jenkins.d/30-coverage.sh
index fc3cbc0..7637b61 100755
--- a/.jenkins.d/30-coverage.sh
+++ b/.jenkins.d/30-coverage.sh
@@ -6,11 +6,27 @@
 
 set -x
 
-# TODO add code coverage support
-if false && [[ $JOB_NAME == *"code-coverage" ]]; then
+if [[ $JOB_NAME == *"code-coverage" ]]; then
     gcovr --object-directory=build \
           --output=build/coverage.xml \
-          --filter="$PWD/(core|tools)" \
+          --exclude="$PWD/tests" \
           --root=. \
           --xml
+
+    # Generate also a detailed HTML output, but using lcov (better results)
+    lcov --quiet \
+         --capture --no-external \
+         --directory . \
+         --rc lcov_branch_coverage=1 \
+         --output-file build/coverage-with-tests.info
+
+    lcov --quiet \
+         --remove build/coverage-with-tests.info "$PWD/tests/*" \
+         --rc lcov_branch_coverage=1 \
+         --output-file build/coverage.info
+
+    genhtml --legend \
+            --rc genhtml_branch_coverage=1 \
+            build/coverage.info \
+            --output-directory build/coverage
 fi
diff --git a/.waf-tools/coverage.py b/.waf-tools/coverage.py
new file mode 100644
index 0000000..ce92883
--- /dev/null
+++ b/.waf-tools/coverage.py
@@ -0,0 +1,22 @@
+# -*- Mode: python; py-indent-offset: 4; indent-tabs-mode: nil; coding: utf-8; -*-
+
+from waflib import TaskGen, Logs
+
+def options(opt):
+    opt.add_option('--with-coverage', action='store_true', default=False, dest='with_coverage',
+                   help='''Set compiler flags for gcc to enable code coverage information''')
+
+def configure(conf):
+    if conf.options.with_coverage:
+        if not conf.options.debug:
+            conf.fatal("Code coverage flags require debug mode compilation (add --debug)")
+        conf.check_cxx(cxxflags=['-fprofile-arcs', '-ftest-coverage', '-fPIC'],
+                       linkflags=['-fprofile-arcs'], uselib_store='GCOV', mandatory=True)
+
+@TaskGen.feature('cxx','cc')
+@TaskGen.after('process_source')
+def add_coverage(self):
+    if getattr(self, 'use', ''):
+        self.use += ' GCOV'
+    else:
+        self.use = 'GCOV'
diff --git a/tests/global-configuration-fixture.cpp b/tests/global-configuration-fixture.cpp
new file mode 100644
index 0000000..35ceeed
--- /dev/null
+++ b/tests/global-configuration-fixture.cpp
@@ -0,0 +1,83 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2017,  Regents of the University of California.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * ndn-tools, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <boost/version.hpp>
+#include <boost/filesystem.hpp>
+
+#include "test-common.hpp"
+
+namespace ndn {
+namespace tools {
+namespace tests {
+
+class GlobalConfigurationFixture : boost::noncopyable
+{
+public:
+  GlobalConfigurationFixture()
+  {
+    if (getenv("HOME") != nullptr) {
+      m_home = getenv("HOME");
+    }
+    if (getenv("NDN_CLIENT_PIB") != nullptr) {
+      m_pib = getenv("NDN_CLIENT_PIB");
+    }
+    if (getenv("NDN_CLIENT_TPM") != nullptr) {
+      m_tpm = getenv("NDN_CLIENT_TPM");
+    }
+
+    boost::filesystem::path dir(TMP_TESTS_PATH);
+    dir /= "test-home";
+    setenv("HOME", dir.generic_string().c_str(), 1);
+
+    if (exists(dir)) {
+      remove_all(dir);
+    }
+
+    setenv("NDN_CLIENT_PIB", ("pib-sqlite3:" + dir.string()).c_str(), 1);
+    setenv("NDN_CLIENT_TPM", ("tpm-file:" + dir.string()).c_str(), 1);
+    create_directories(dir);
+  }
+
+  ~GlobalConfigurationFixture()
+  {
+    if (!m_home.empty()) {
+      setenv("HOME", m_home.c_str(), 1);
+    }
+    if (!m_pib.empty()) {
+      setenv("NDN_CLIENT_PIB", m_pib.c_str(), 1);
+    }
+    if (!m_tpm.empty()) {
+      setenv("NDN_CLIENT_TPM", m_tpm.c_str(), 1);
+    }
+  }
+
+private:
+  std::string m_home;
+  std::string m_pib;
+  std::string m_tpm;
+};
+
+BOOST_GLOBAL_FIXTURE(GlobalConfigurationFixture)
+#if (BOOST_VERSION >= 105900)
+;
+#endif // BOOST_VERSION >= 105900
+
+} // namespace tests
+} // namespace tools
+} // namespace ndn
diff --git a/wscript b/wscript
index 07a7524..42cb690 100644
--- a/wscript
+++ b/wscript
@@ -9,7 +9,8 @@
 
 def options(opt):
     opt.load(['compiler_cxx', 'gnu_dirs'])
-    opt.load(['default-compiler-flags', 'sanitizers', 'sphinx_build', 'boost'],
+    opt.load(['default-compiler-flags', 'coverage', 'sanitizers', 'boost',
+              'sphinx_build'],
              tooldir=['.waf-tools'])
 
     opt.add_option('--with-tests', action='store_true', default=False,
@@ -33,6 +34,9 @@
         boost_libs += ' unit_test_framework'
     conf.check_boost(lib=boost_libs)
 
+    # Loading "late" to prevent tests from being compiled with profiling flags
+    conf.load('coverage')
+
     conf.recurse('tools')
 
     conf.load('sanitizers')