#include "folly/Subprocess.h"
#include <unistd.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <boost/container/flat_set.hpp>
#include <glog/logging.h>
#include <gtest/gtest.h>
+#include "folly/Exception.h"
#include "folly/Format.h"
#include "folly/experimental/Gen.h"
#include "folly/experimental/FileGen.h"
EXPECT_THROW(proc.waitChecked(), CalledProcessError);
}
+#define EXPECT_SPAWN_ERROR(err, errMsg, cmd, ...) \
+ do { \
+ try { \
+ Subprocess proc(std::vector<std::string>{ (cmd), ## __VA_ARGS__ }); \
+ ADD_FAILURE() << "expected an error when running " << (cmd); \
+ } catch (const SubprocessSpawnError& ex) { \
+ EXPECT_EQ((err), ex.errnoValue()); \
+ if (StringPiece(ex.what()).find(errMsg) == StringPiece::npos) { \
+ ADD_FAILURE() << "failed to find \"" << (errMsg) << \
+ "\" in exception: \"" << ex.what() << "\""; \
+ } \
+ } \
+ } while (0)
+
TEST(SimpleSubprocessTest, ExecFails) {
- Subprocess proc(std::vector<std::string>{ "/no/such/file" });
- EXPECT_EQ(127, proc.wait().exitStatus());
+ EXPECT_SPAWN_ERROR(ENOENT, "failed to execute /no/such/file:",
+ "/no/such/file");
+ EXPECT_SPAWN_ERROR(EACCES, "failed to execute /etc/passwd:",
+ "/etc/passwd");
+ EXPECT_SPAWN_ERROR(ENOTDIR, "failed to execute /etc/passwd/not/a/file:",
+ "/etc/passwd/not/a/file");
}
TEST(SimpleSubprocessTest, ShellExitsSuccesssfully) {
EXPECT_EQ(1, proc.wait().exitStatus());
}
+namespace {
+boost::container::flat_set<int> getOpenFds() {
+ auto pid = getpid();
+ auto dirname = to<std::string>("/proc/", pid, "/fd");
+
+ boost::container::flat_set<int> fds;
+ for (fs::directory_iterator it(dirname);
+ it != fs::directory_iterator();
+ ++it) {
+ int fd = to<int>(it->path().filename().native());
+ fds.insert(fd);
+ }
+ return fds;
+}
+
+template<class Runnable>
+void checkFdLeak(const Runnable& r) {
+ // Get the currently open fds. Check that they are the same both before and
+ // after calling the specified function. We read the open fds from /proc.
+ // (If we wanted to work even on systems that don't have /proc, we could
+ // perhaps create and immediately close a socket both before and after
+ // running the function, and make sure we got the same fd number both times.)
+ auto fdsBefore = getOpenFds();
+ r();
+ auto fdsAfter = getOpenFds();
+ EXPECT_EQ(fdsAfter.size(), fdsBefore.size());
+}
+}
+
+// Make sure Subprocess doesn't leak any file descriptors
+TEST(SimpleSubprocessTest, FdLeakTest) {
+ // Normal execution
+ checkFdLeak([] {
+ Subprocess proc("true");
+ EXPECT_EQ(0, proc.wait().exitStatus());
+ });
+ // Normal execution with pipes
+ checkFdLeak([] {
+ Subprocess proc("echo foo; echo bar >&2",
+ Subprocess::pipeStdout() | Subprocess::pipeStderr());
+ auto p = proc.communicate(Subprocess::readStdout() |
+ Subprocess::readStderr());
+ EXPECT_EQ("foo\n", p.first);
+ EXPECT_EQ("bar\n", p.second);
+ proc.waitChecked();
+ });
+
+ // Test where the exec call fails()
+ checkFdLeak([] {
+ EXPECT_SPAWN_ERROR(ENOENT, "failed to execute", "/no/such/file");
+ });
+ // Test where the exec call fails() with pipes
+ checkFdLeak([] {
+ try {
+ Subprocess proc(std::vector<std::string>({"/no/such/file"}),
+ Subprocess::pipeStdout().stderr(Subprocess::PIPE));
+ ADD_FAILURE() << "expected an error when running /no/such/file";
+ } catch (const SubprocessSpawnError& ex) {
+ EXPECT_EQ(ENOENT, ex.errnoValue());
+ }
+ });
+}
+
TEST(ParentDeathSubprocessTest, ParentDeathSignal) {
// Find out where we are.
static constexpr size_t pathLength = 2048;
EXPECT_EQ(std::string::npos, p.first.find_first_not_of('X'));
proc.waitChecked();
}
-