wsd: support code-coverage report via --with-coverage

This adds support for code-coverage HTML reporting.
To achieve this, we must use file-linking in jails
so that we can update the coverage data (.gcda files)
from the jails. This means that creating jails is
slower than with bind-mounting and we need to
account for that in our timeouts.

We also can't kill child processes with SIGKILL,
which is un-catchable. Instead, we use SIGTERM
and dump the profile data before exiting.

Change-Id: I16fa534f6ed42f7133014d841bb024423315e0a4
Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
This commit is contained in:
Ashod Nakashian 2022-03-13 18:28:31 -04:00 committed by Henry Castro
parent 551454c9f5
commit 5c6516e4e4
12 changed files with 213 additions and 6 deletions

5
.gitignore vendored
View file

@ -64,6 +64,11 @@ coolwsd.log
*.lo
*.mo
gcov/*
*.gcno
*.gcda
*.gcov
# browser
browser/debug
browser/release

View file

@ -327,6 +327,9 @@ noinst_HEADERS = $(wsd_headers) $(shared_headers) $(kit_headers) \
test/WOPIUploadConflictCommon.hpp \
test/helpers.hpp
GIT_BRANCH := $(shell git symbolic-ref --short HEAD)
GIT_HASH := $(shell git log -1 --format=%h)
dist-hook:
git log -1 --format=%h > $(distdir)/dist_git_hash 2> /dev/null || rm $(distdir)/dist_git_hash
mkdir -p $(distdir)/bundled/include/LibreOfficeKit/
@ -418,6 +421,8 @@ clean-local:
rm -rf "${top_srcdir}/loleaflet"
rm -rf loolconfig loolconvert loolforkit loolmap loolmount # kill old binaries
rm -rf loolwsd loolwsd_fuzzer coolwsd_fuzzer loolstress loolsocketdump
rm -rf ${abs_top_srcdir}/gcov
find . -iname "*.gc??" -delete
if ENABLE_DEBUG
# can write to /tmp/coolwsd.log
@ -612,5 +617,22 @@ stress:
$(stress_file) $(trace_dir)/writer-hello-shape.txt \
$(stress_file) $(trace_dir)/writer-quick.txt
if ENABLE_CODE_COVERAGE
GEN_COVERAGE_COMMAND=mkdir -p ${abs_top_srcdir}/gcov && \
lcov --no-external --capture --rc 'lcov_excl_line=' --rc 'lcov_excl_br_line=LOG_|TST_|LOK_|WSD_|TRANSITION|assert' \
--compat libtool=on --directory ${abs_top_srcdir}/. --output-file ${abs_top_srcdir}/gcov/cool.coverage.test.info && \
genhtml --prefix ${abs_top_srcdir}/. --ignore-errors source ${abs_top_srcdir}/gcov/cool.coverage.test.info \
--legend --title "${GIT_BRANCH} @ ${GIT_HASH}" --output-directory=${abs_top_srcdir}/gcov/html && \
echo "Code-Coverage report generated in ${abs_top_srcdir}/gcov/html"
else
GEN_COVERAGE_COMMAND=true
endif
check: check-recursive
$(GEN_COVERAGE_COMMAND)
coverage-report:
$(GEN_COVERAGE_COMMAND)
czech: check
@echo "This should do something much cooler"

View file

@ -22,6 +22,8 @@ constexpr int DEFAULT_CLIENT_PORT_NUMBER = 9980;
#if VALGRIND_COOLFORKIT
constexpr int TRACE_MULTIPLIER = 20;
#elif CODE_COVERAGE
constexpr int TRACE_MULTIPLIER = 5;
#else
constexpr int TRACE_MULTIPLIER = 1;
#endif

View file

@ -317,6 +317,10 @@ namespace SigUtil
SocketPoll::wakeupWorld();
else
{
#if CODE_COVERAGE
__gcov_dump();
#endif
::signal (signal, SIG_DFL);
::raise (signal);
}

View file

@ -1095,6 +1095,11 @@ namespace Util
{
LOG_FTL("Forced Exit with code: " << code);
Log::shutdown();
#if CODE_COVERAGE
__gcov_dump();
#endif
std::_Exit(code);
}

View file

@ -42,6 +42,15 @@
#include <StringVector.hpp>
#if CODE_COVERAGE
extern "C"
{
void __gcov_reset(void);
void __gcov_flush(void);
void __gcov_dump(void);
}
#endif
/// Format seconds with the units suffix until we migrate to C++20.
inline std::ostream& operator<<(std::ostream& os, const std::chrono::seconds& s)
{

View file

@ -299,6 +299,11 @@ AC_ARG_WITH([infobar-url],
AS_HELP_STRING([--with-infobar-url=<url>],
[Infobar URL.]))
AC_ARG_WITH([coverage],
AS_HELP_STRING([--with-coverage=<gcov>],
[Enable a code-coverage method. Currently only gcov is supported (works with both gcc and clang).
Output HTML in gcov/ directory.]))
AC_ARG_WITH([sanitizer],
AS_HELP_STRING([--with-sanitizer],
[Enable one or more compatible sanitizers. E.g. --with-sanitizer=address,undefined,leak]))
@ -843,6 +848,23 @@ else
AC_MSG_RESULT([no])
fi
# Code Coverage.
AC_MSG_CHECKING([whether code-coverage is enabled])
if test "x$with_coverage" != "x"; then
AC_MSG_RESULT([yes ($with_coverage)])
CODE_COVERAGE=1
CXXFLAGS="$CXXFLAGS --coverage"
CFLAGS="$CFLAGS --coverage"
LDFLAGS="$LDFLAGS -Wl,--dynamic-list-data --coverage"
coverage_msg="Code-Coverage is enabled"
else
AC_MSG_RESULT([no])
CODE_COVERAGE=0
coverage_msg="Code-Coverage is disabled"
fi
AC_DEFINE_UNQUOTED([CODE_COVERAGE],[$CODE_COVERAGE],[Define to 1 if this is a code-coverage build.])
AM_CONDITIONAL([ENABLE_CODE_COVERAGE], [test "$CODE_COVERAGE" = "1"])
# Fuzzing.
AC_MSG_CHECKING([whether to build fuzzers])
if test "$enable_fuzzers" = "yes"; then
@ -1585,6 +1607,7 @@ Configuration:
Browsersync $browsersync_msg
cypress $cypress_msg
C++ compiler flags $CXXFLAGS
Code-Coverage $coverage_msg
\$ make # to compile"
if test -n "$with_lo_path"; then

View file

@ -748,7 +748,7 @@ int main(int argc, char** argv)
const int parentPid = getppid();
LOG_INF("ForKit process is ready. Parent: " << parentPid);
while (!SigUtil::getTerminationFlag())
while (!SigUtil::getShutdownRequestFlag() && !SigUtil::getTerminationFlag())
{
UnitKit::get().invokeForKitTest();

View file

@ -499,6 +499,104 @@ namespace
}
}
#if CODE_COVERAGE
std::string childRootForGCDAFiles;
std::string sourceForGCDAFiles;
std::string destForGCDAFiles;
int linkGCDAFilesFunction(const char* fpath, const struct stat*, int typeflag,
struct FTW* /*ftwbuf*/)
{
if (strcmp(fpath, sourceForGCDAFiles.c_str()) == 0)
{
LOG_TRC("nftw: Skipping redundant path: " << fpath);
return FTW_CONTINUE;
}
if (Util::startsWith(fpath, childRootForGCDAFiles))
{
LOG_TRC("nftw: Skipping childRoot subtree: " << fpath);
return FTW_SKIP_SUBTREE;
}
assert(fpath[strlen(sourceForGCDAFiles.c_str())] == '/');
const char* relativeOldPath = fpath + strlen(sourceForGCDAFiles.c_str()) + 1;
const Poco::Path newPath(destForGCDAFiles, Poco::Path(relativeOldPath));
switch (typeflag)
{
case FTW_F:
case FTW_SLN:
{
const char* dot = strrchr(relativeOldPath, '.');
if (dot && !strcmp(dot, ".gcda"))
{
Poco::File(newPath.parent()).createDirectories();
if (link(fpath, newPath.toString().c_str()) != 0)
{
LOG_SYS("nftw: Failed to link [" << fpath << "] -> [" << newPath.toString()
<< ']');
}
}
}
break;
case FTW_D:
case FTW_SL:
break;
case FTW_DNR:
LOG_ERR("nftw: Cannot read directory '" << fpath << '\'');
break;
case FTW_NS:
LOG_ERR("nftw: stat failed for '" << fpath << '\'');
break;
default:
LOG_FTL("nftw: unexpected typeflag: '" << typeflag);
assert(!"nftw: unexpected typeflag.");
break;
}
return FTW_CONTINUE;
}
/// Link .gcda (gcov) files from the src directory into the jail.
/// We need this so we can easily extract the profile data from within
/// the jail. Otherwise, we lose coverage info of the kit process.
void linkGCDAFiles(const std::string& destPath)
{
Poco::Path sourcePathInJail(destPath);
const auto sourcePath = std::string(DEBUG_ABSSRCDIR);
sourcePathInJail.append(sourcePath);
Poco::File(sourcePathInJail).createDirectories();
LOG_INF("Linking .gcda files from " << sourcePath << " -> " << sourcePathInJail.toString());
const auto childRootPtr = std::getenv("BASE_CHILD_ROOT");
if (childRootPtr == nullptr || strlen(childRootPtr) == 0)
{
LOG_ERR("Cannot collect code-coverage stats for the Kit processes. BASE_CHILD_ROOT "
"envar missing.");
return;
}
// Trim the trailing /.
const std::string childRoot = childRootPtr;
const size_t last = childRoot.find_last_not_of('/');
if (last != std::string::npos)
childRootForGCDAFiles = childRoot.substr(0, last + 1);
else
childRootForGCDAFiles = childRoot;
sourceForGCDAFiles = sourcePath;
destForGCDAFiles = sourcePathInJail.toString() + '/';
LOG_INF("nftw .gcda files from " << sourceForGCDAFiles << " -> " << destForGCDAFiles << " ("
<< childRootForGCDAFiles << ')');
if (nftw(sourcePath.c_str(), linkGCDAFilesFunction, 10, FTW_ACTIONRETVAL | FTW_PHYS) == -1)
{
LOG_ERR("linkGCDAFiles: nftw() failed for '" << sourcePath << '\'');
}
}
#endif
#ifndef __FreeBSD__
void dropCapability(cap_value_t capability)
{
@ -2595,6 +2693,12 @@ void lokit_main(
bool bindMount = JailUtil::isBindMountingEnabled();
if (bindMount)
{
#if CODE_COVERAGE
// Code coverage is not supported with bind-mounting.
LOG_ERR("Mounting is not compatible with code-coverage.");
assert(!"Mounting is not compatible with code-coverage.");
#endif // CODE_COVERAGE
if (!mountJail())
{
LOG_INF("Cleaning up jail before linking/copying.");
@ -2618,6 +2722,11 @@ void lokit_main(
linkOrCopy(loTemplate, loJailDestPath, linkablePath, LinkOrCopyType::LO);
#if CODE_COVERAGE
// Link the .gcda files.
linkGCDAFiles(jailPathStr);
#endif
// Update the dynamic files inside the jail.
if (!JailUtil::SysTemplate::updateDynamicFiles(jailPathStr))
{

View file

@ -24,7 +24,7 @@ public:
: UnitWSD("UnitTimeout")
, _timedOut(false)
{
setTimeout(std::chrono::milliseconds(10));
setTimeout(std::chrono::seconds(1));
}
virtual void timeout() override

View file

@ -2268,6 +2268,10 @@ void COOLWSD::innerInitialize(Application& self)
if (ChildRoot[ChildRoot.size() - 1] != '/')
ChildRoot += '/';
#if CODE_COVERAGE
::setenv("BASE_CHILD_ROOT", Poco::Path(ChildRoot).absolute().toString().c_str(), 1);
#endif
// Create a custom sub-path for parallelized unit tests.
if (UnitBase::isUnitTesting())
{
@ -2307,9 +2311,18 @@ void COOLWSD::innerInitialize(Application& self)
// Initialize the config subsystem too.
config::initialize(&config());
bool bindMount = getConfigValue<bool>(conf, "mount_jail_tree", true);
#if CODE_COVERAGE
// Code coverage is not supported with bind-mounting.
if (bindMount)
{
LOG_WRN("Mounting is not compatible with code-coverage. Disabling.");
bindMount = false;
}
#endif // CODE_COVERAGE
// Setup the jails.
JailUtil::setupChildRoot(getConfigValue<bool>(conf, "mount_jail_tree", true), ChildRoot,
SysTemplate);
JailUtil::setupChildRoot(bindMount, ChildRoot, SysTemplate);
LOG_DBG("FileServerRoot before config: " << FileServerRoot);
FileServerRoot = getPathFromConfig("file_server_root_path");
@ -5436,7 +5449,12 @@ int COOLWSD::innerMain()
#if !defined(KIT_IN_PROCESS) && !MOBILEAPP
// Terminate child processes
LOG_INF("Requesting forkit process " << ForKitProcId << " to terminate.");
SigUtil::killChild(ForKitProcId, SIGKILL);
#if CODE_COVERAGE
constexpr auto signal = SIGTERM;
#else
constexpr auto signal = SIGKILL;
#endif
SigUtil::killChild(ForKitProcId, signal);
#endif
Server->stopPrisoners();
@ -5559,6 +5577,11 @@ int COOLWSD::main(const std::vector<std::string>& /*args*/)
UnitWSD::get().returnValue(returnValue);
LOG_INF("Process [coolwsd] finished with exit status: " << returnValue);
#if CODE_COVERAGE
__gcov_dump();
#endif
return returnValue;
}

View file

@ -117,7 +117,12 @@ public:
if (::kill(_pid, 0) == 0)
{
LOG_INF("Killing child [" << _pid << "].");
if (!SigUtil::killChild(_pid, SIGKILL))
#if CODE_COVERAGE
constexpr auto signal = SIGTERM;
#else
constexpr auto signal = SIGKILL;
#endif
if (!SigUtil::killChild(_pid, signal))
{
LOG_ERR("Cannot terminate lokit [" << _pid << "]. Abandoning.");
}