From 0dd22bd5cbae00545611551e26a24ab52b8def7a Mon Sep 17 00:00:00 2001 From: Simon Kagstrom Date: Fri, 3 Nov 2017 13:21:17 +0100 Subject: [PATCH] Issue #224: bash-engine: Show uncovered scripts in report By default kcov scans the binary directory for bash scripts. --- doc/kcov.1 | 7 ++++ src/configuration.cc | 18 +++++++++- src/engines/bash-engine.cc | 68 +++++++++++++++++++++++++++++++++++++- tests/tools/bash.py | 32 ++++++++++++++++++ 4 files changed, 123 insertions(+), 2 deletions(-) diff --git a/doc/kcov.1 b/doc/kcov.1 index 466609e4..8fc0536d 100644 --- a/doc/kcov.1 +++ b/doc/kcov.1 @@ -103,6 +103,13 @@ the PS4 environment variable, or DEBUG for use of the DEBUG trap. Handle invocations of /bin/sh scripts via using a LD_PRELOADed library that replaces execve (i.e., /bin/sh is executed as /bin/bash). Does not work well on some systems, so the default is not to use this. .TP +\fB\-\-bash\-dont\-parse\-binary\-dir +Kcov parses the directory of the binary for other scripts and add these to the report. If you don't +want this behavior, this option turns that off. +.TP +\fB\-\-bash\-parse\-files\-in\-dir\fP=\fIP1\fP[\fI,P2\fP...] +Parse directories for bash scripts. +.TP \fB\-\-replace\-src\-path\fP=\fIP1\fP:\fIP2\fP Replace source file path P1 with P2, if found. .TP diff --git a/src/configuration.cc b/src/configuration.cc index 35e00b04..0e996c45 100644 --- a/src/configuration.cc +++ b/src/configuration.cc @@ -135,6 +135,8 @@ class Configuration : public IConfiguration {"debug", required_argument, 0, 'D'}, {"debug-force-bash-stderr", no_argument, 0, 'd'}, {"bash-handle-sh-invocation", no_argument, 0, 's'}, + {"bash-dont-parse-binary-dir", no_argument, 0, 'j'}, + {"bash-parse-files-in-dirs", required_argument, 0, 'J'}, {"replace-src-path", required_argument, 0, 'R'}, {"collect-only", no_argument, 0, 'C'}, {"report-only", no_argument, 0, 'r'}, @@ -402,6 +404,16 @@ class Configuration : public IConfiguration setKey("high-limit", stoul(vec[1])); break; } + case 'j': + setKey("bash-parse-binary-dir", 0); + break; + case 'J': + { + StrVecMap_t bashFilesInPath = getCommaSeparatedList(std::string(optarg)); + + setKey("bash-parse-file-dir", bashFilesInPath); + break; + } case 'R': { std::string tmpArg = std::string(optarg); size_t tokenPosFront = tmpArg.find_first_of(":"); @@ -551,6 +563,8 @@ class Configuration : public IConfiguration setKey("bash-handle-sh-invocation", 0); setKey("bash-use-basic-parser", 0); setKey("bash-use-ps4", 1); + setKey("bash-parse-file-dir", StrVecMap_t()); + setKey("bash-parse-binary-dir", 1); setKey("verify", 0); setKey("command-name", ""); setKey("merged-name", "[merged]"); @@ -672,7 +686,9 @@ class Configuration : public IConfiguration " default: %s\n" " --bash-method=method Bash coverage collection method, PS4 (default) or DEBUG\n" " --bash-handle-sh-invocation Try to handle #!/bin/sh scripts by a LD_PRELOAD\n" - " execve replacement. Buggy on some systems\n", + " execve replacement. Buggy on some systems\n" + " --bash-dont-parse-binary-dir Don't parse the binary directory for other scripts\n" + " --bash-parse-files-in-dir=dir[,...] Parse bash scripts in dir(s)\n", keyAsInt("path-strip-level"), keyAsInt("output-interval"), getConfigurableValues(), keyAsString("python-command").c_str(), keyAsString("bash-command").c_str(), diff --git a/src/engines/bash-engine.cc b/src/engines/bash-engine.cc index b97fd126..cc1a372b 100644 --- a/src/engines/bash-engine.cc +++ b/src/engines/bash-engine.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -57,6 +58,7 @@ class BashEngine : public ScriptEngineBase bool start(IEventListener &listener, const std::string &executable) { + IConfiguration &conf = IConfiguration::getInstance(); int stderrPipe[2]; int stdoutPipe[2]; @@ -127,7 +129,6 @@ class BashEngine : public ScriptEngineBase } } else if (m_child == 0) { - IConfiguration &conf = IConfiguration::getInstance(); const char **argv = conf.getArgv(); unsigned int argc = conf.getArgc(); int xtraceFd = 782; // Typical bash users use 3,4 etc but not high fd numbers (?) @@ -201,6 +202,21 @@ class BashEngine : public ScriptEngineBase return false; } + + std::vector dirsToParse = conf.keyAsList("bash-parse-file-dir"); + + for (std::vector::iterator it = dirsToParse.begin(); + it != dirsToParse.end(); + ++it) + { + parseDirectoryForFiles(*it); + } + + // Parse the directory where the script-under-test is for other bash scripts + if (conf.keyAsInt("bash-parse-binary-dir")) + { + parseDirectoryForFiles(conf.keyAsString("binary-path")); + } parseFile(executable); return true; @@ -349,6 +365,56 @@ class BashEngine : public ScriptEngineBase private: + void parseDirectoryForFiles(const std::string &base) + { + DIR *dir = ::opendir(base.c_str()); + if (!dir) + { + error("Can't open directory %s\n", base.c_str()); + return; + } + + // Loop through the directory structure + struct dirent *de; + for (de = ::readdir(dir); de; de = ::readdir(dir)) + { + std::string cur = base + "/" + de->d_name; + + if (strcmp(de->d_name, ".") == 0) + continue; + + if (strcmp(de->d_name, "..") == 0) + continue; + + struct stat st; + + if (lstat(cur.c_str(), &st) < 0) + continue; + + if (S_ISDIR(st.st_mode)) + { + parseDirectoryForFiles(cur); + } + else + { + size_t sz; + uint8_t *p = (uint8_t *)read_file(&sz, "%s", cur.c_str()); + + if (p) + { + // Bash file? + if (matchParser(cur, p, sz) != match_none) + { + parseFile(cur); + } + + } + free((void *)p); + } + } + ::closedir(dir); + } + // Printout lines to stdout, except kcov markers void handleStdout() { diff --git a/tests/tools/bash.py b/tests/tools/bash.py index e0e058f0..e7ad8840 100644 --- a/tests/tools/bash.py +++ b/tests/tools/bash.py @@ -279,3 +279,35 @@ def runTest(self): assert parse_cobertura.hitsPerLine(dom, "other.sh", 48) == None assert parse_cobertura.hitsPerLine(dom, "other.sh", 49) == None assert parse_cobertura.hitsPerLine(dom, "other.sh", 51) == 1 + +# Issue #224 +class bash_can_find_non_executed_scripts(testbase.KcovTestCase): + def runTest(self): + self.setUp() + rv,o = self.do(testbase.kcov + " " + testbase.outbase + "/kcov " + testbase.sources + "/tests/bash/first-dir/a.sh 5") + + dom = parse_cobertura.parseFile(testbase.outbase + "/kcov/a.sh/cobertura.xml") + assert parse_cobertura.hitsPerLine(dom, "a.sh", 5) == 1 + # Not executed + assert parse_cobertura.hitsPerLine(dom, "c.sh", 3) == 0 + +class bash_can_find_non_executed_scripts_manually(testbase.KcovTestCase): + def runTest(self): + self.setUp() + rv,o = self.do(testbase.kcov + " --bash-parse-files-in-dir=" + testbase.sources + "/tests/bash " + testbase.outbase + "/kcov " + testbase.sources + "/tests/bash/first-dir/a.sh 5") + + dom = parse_cobertura.parseFile(testbase.outbase + "/kcov/a.sh/cobertura.xml") + # Not executed + assert parse_cobertura.hitsPerLine(dom, "c.sh", 3) == 0 + assert parse_cobertura.hitsPerLine(dom, "other.sh", 3) == 0 + +class bash_can_ignore_non_executed_scripts(testbase.KcovTestCase): + def runTest(self): + self.setUp() + rv,o = self.do(testbase.kcov + " --bash-dont-parse-binary-dir " + testbase.outbase + "/kcov " + testbase.sources + "/tests/bash/first-dir/a.sh 5") + + dom = parse_cobertura.parseFile(testbase.outbase + "/kcov/a.sh/cobertura.xml") + assert parse_cobertura.hitsPerLine(dom, "a.sh", 5) == 1 + # Not included in report + assert parse_cobertura.hitsPerLine(dom, "c.sh", 3) == None + assert parse_cobertura.hitsPerLine(dom, "other.sh", 3) == None