| // Protocol Buffers - Google's data interchange format | |
| // Copyright 2026 Google LLC. All rights reserved. | |
| // | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file or at | |
| // https://developers.google.com/open-source/licenses/bsd | |
| // Hermetic tests for ForkPipeRunner's process management. The testee is | |
| // /bin/sh running small scripts that stand in for a well-behaved, a stubborn, | |
| // a crashing, a signal-killed and a hung conformance testee. The pipe protocol | |
| // is a 4-byte little-endian length prefix followed by the payload in each | |
| // direction, so `cat` is a testee that echoes every request back verbatim, and | |
| // a 3-byte request is exactly 7 bytes on the wire. | |
| // TODO: b/418427266 - add Windows coverage; these tests drive the testee with | |
| // /bin/sh and inspect process groups. | |
| #ifndef _WIN32 | |
| #include "conformance/fork_pipe_runner.h" | |
| #include <poll.h> | |
| #include <signal.h> | |
| #include <sys/types.h> | |
| #include <sys/wait.h> | |
| #include <unistd.h> | |
| #include <cerrno> | |
| #include <cstddef> | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include <gmock/gmock.h> | |
| #include <gtest/gtest.h> | |
| #include "absl/base/log_severity.h" | |
| #include "absl/log/absl_check.h" | |
| #include "absl/log/scoped_mock_log.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/time/clock.h" | |
| #include "absl/time/time.h" | |
| #include "absl/types/optional.h" | |
| #include "conformance/conformance.pb.h" | |
| namespace google { | |
| namespace protobuf { | |
| namespace conformance { | |
| namespace { | |
| using ::testing::_; | |
| using ::testing::HasSubstr; | |
| using ::testing::Not; | |
| constexpr absl::string_view kShell = "/bin/sh"; | |
| // True iff this process has no child processes left, live or zombie. The | |
| // test process has no children other than the ones ForkPipeRunner spawns. | |
| bool NoChildRemains() { | |
| return waitpid(-1, nullptr, WNOHANG) == -1 && errno == ECHILD; | |
| } | |
| // A runner for /bin/sh running `script`. Tests of the SIGKILL fallback and of | |
| // the read-timeout path pass short timeouts so as not to wait out the defaults. | |
| std::unique_ptr<ForkPipeRunner> MakeRunner(const std::string& script, | |
| ForkPipeRunnerOptions options = {}) { | |
| const std::vector<std::string> args = {"-c", script}; | |
| return std::make_unique<ForkPipeRunner>(kShell, args, options); | |
| } | |
| ForkPipeRunnerOptions WithShutdownGracePeriod(absl::Duration grace_period) { | |
| ForkPipeRunnerOptions options; | |
| options.shutdown_grace_period = grace_period; | |
| return options; | |
| } | |
| ForkPipeRunnerOptions WithReadTimeout(absl::Duration read_timeout) { | |
| ForkPipeRunnerOptions options; | |
| options.read_timeout = read_timeout; | |
| return options; | |
| } | |
| ::conformance::ConformanceResponse ParseResponse( | |
| const std::string& serialized) { | |
| ::conformance::ConformanceResponse response; | |
| EXPECT_TRUE(response.ParseFromString(serialized)); | |
| return response; | |
| } | |
| // A pipe that nobody writes to, whose write end is inherited by the testee and | |
| // by everything the testee forks. Its read end therefore reports EOF exactly | |
| // when every process holding the write end has died, which detects a leaked | |
| // grandchild without depending on who reaps it or on pid reuse. | |
| class InheritedPipe { | |
| public: | |
| InheritedPipe() { | |
| int fds[2]; | |
| ABSL_PCHECK(pipe(fds) == 0); | |
| read_fd_ = fds[0]; | |
| write_fd_ = fds[1]; | |
| } | |
| ~InheritedPipe() { | |
| CloseWriteEnd(); | |
| close(read_fd_); | |
| } | |
| // To be called once the testee has been spawned, so that this process no | |
| // longer holds the write end open itself. | |
| void CloseWriteEnd() { | |
| if (write_fd_.has_value()) { | |
| close(*write_fd_); | |
| write_fd_.reset(); | |
| } | |
| } | |
| // True iff every process holding the write end has died within `timeout`. | |
| bool WritersAreGone(absl::Duration timeout) const { | |
| pollfd pfd = {}; | |
| pfd.fd = read_fd_; | |
| pfd.events = POLLIN; | |
| if (poll(&pfd, 1, static_cast<int>(absl::ToInt64Milliseconds(timeout))) <= | |
| 0) { | |
| return false; | |
| } | |
| char c; | |
| return read(read_fd_, &c, 1) == 0; | |
| } | |
| private: | |
| int read_fd_; | |
| // nullopt once CloseWriteEnd() has closed it. | |
| absl::optional<int> write_fd_; | |
| }; | |
| // Destroys `runner` and returns how long that took. | |
| absl::Duration TimeDestruction(std::unique_ptr<ForkPipeRunner> runner) { | |
| const absl::Time start = absl::Now(); | |
| runner.reset(); | |
| return absl::Now() - start; | |
| } | |
| // While alive, has the kernel reap this process's children as they exit, | |
| // which is what ignoring SIGCHLD means. Some launchers do that, and an ignored | |
| // SIGCHLD survives exec, so a runner can find itself in this situation. | |
| class ScopedAutoReap { | |
| public: | |
| ScopedAutoReap() { | |
| struct sigaction ignore = {}; | |
| ignore.sa_handler = SIG_IGN; | |
| sigemptyset(&ignore.sa_mask); | |
| ABSL_PCHECK(sigaction(SIGCHLD, &ignore, &saved_) == 0); | |
| } | |
| ~ScopedAutoReap() { ABSL_PCHECK(sigaction(SIGCHLD, &saved_, nullptr) == 0); } | |
| private: | |
| struct sigaction saved_; | |
| }; | |
| TEST(ForkPipeRunnerTest, CooperativeTesteeIsReaped) { | |
| // `cat` echoes the length-prefixed request straight back, and exits on EOF | |
| // like a real testee does. | |
| auto runner = MakeRunner("exec cat"); | |
| EXPECT_EQ(runner->RunTest("t", "abc"), "abc"); | |
| // A cooperative testee must not be made to wait for the grace period. | |
| EXPECT_LT(TimeDestruction(std::move(runner)), absl::Seconds(5)); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, UnusedRunnerSpawnsNothing) { | |
| // The testee is only spawned by the first RunTest(), so a runner that is | |
| // never used has nothing to shut down. | |
| auto runner = MakeRunner("exec cat"); | |
| EXPECT_LT(TimeDestruction(std::move(runner)), absl::Seconds(1)); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, EmptyRequestAndResponseRoundTrip) { | |
| // A zero-length payload is just a zero length prefix in each direction. | |
| auto runner = MakeRunner("exec cat"); | |
| EXPECT_EQ(runner->RunTest("t", ""), ""); | |
| EXPECT_EQ(runner->RunTest("t", "abc"), "abc"); | |
| } | |
| TEST(ForkPipeRunnerTest, PayloadsLargerThanThePipeBufferRoundTrip) { | |
| // A pipe holds 64 KiB on Linux. Like a real testee, the script reads the | |
| // whole request before it writes anything, so neither side deadlocks on a | |
| // full pipe (`cat` would). The response is 100000 bytes, announced by its | |
| // little-endian length prefix, so the runner has to assemble it from | |
| // several reads. | |
| constexpr size_t kSize = 100000; | |
| auto runner = MakeRunner( | |
| "head -c 100004 >/dev/null; printf '\\240\\206\\001\\000'; " | |
| "head -c 100000 /dev/zero"); | |
| const std::string response = runner->RunTest("t", std::string(kSize, 'x')); | |
| EXPECT_EQ(response.size(), kSize); | |
| EXPECT_EQ(response.find_first_not_of('\0'), std::string::npos); | |
| } | |
| TEST(ForkPipeRunnerTest, PipesAreNotInheritedByAnotherTestee) { | |
| // The second testee is forked while the first one's pipes are open. Were | |
| // it to inherit the write end of the first testee's stdin, the first `cat` | |
| // would not see EOF when its runner shuts down and would have to be killed | |
| // after the grace period. | |
| constexpr absl::Duration kGrace = absl::Seconds(2); | |
| auto first = MakeRunner("exec cat", WithShutdownGracePeriod(kGrace)); | |
| EXPECT_EQ(first->RunTest("t", "abc"), "abc"); | |
| auto second = MakeRunner("exec cat"); | |
| EXPECT_EQ(second->RunTest("t", "def"), "def"); | |
| EXPECT_LT(TimeDestruction(std::move(first)), kGrace); | |
| EXPECT_EQ(second->RunTest("t", "ghi"), "ghi"); | |
| second.reset(); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, StubbornTesteeIsKilled) { | |
| // Answers one request, then ignores EOF on stdin and lives forever. | |
| constexpr absl::Duration kGrace = absl::Milliseconds(100); | |
| auto runner = MakeRunner("trap '' PIPE; cat; while :; do sleep 1; done", | |
| WithShutdownGracePeriod(kGrace)); | |
| EXPECT_EQ(runner->RunTest("t", "abc"), "abc"); | |
| // The destructor waits out the grace period, then SIGKILLs the testee and | |
| // reaps it promptly. | |
| const absl::Duration elapsed = TimeDestruction(std::move(runner)); | |
| EXPECT_GE(elapsed, kGrace); | |
| EXPECT_LT(elapsed, kGrace + absl::Seconds(5)); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, StubbornTesteesGrandchildIsKilledToo) { | |
| InheritedPipe pipe; | |
| // Forks a grandchild that lives forever, answers one request, then ignores | |
| // EOF itself. The grandchild is a wrapper script's real testee stand-in; | |
| // it is in the testee's process group but is not our child. | |
| constexpr absl::Duration kGrace = absl::Milliseconds(100); | |
| auto runner = MakeRunner("sleep 1000 & cat; exec sleep 1000", | |
| WithShutdownGracePeriod(kGrace)); | |
| EXPECT_EQ(runner->RunTest("t", "abc"), "abc"); | |
| pipe.CloseWriteEnd(); | |
| ASSERT_FALSE(pipe.WritersAreGone(absl::ZeroDuration())) | |
| << "the testee should be holding the pipe open"; | |
| runner.reset(); | |
| // Killing only the direct child would leave `sleep 1000 &` running (and | |
| // holding the pipe's write end), so EOF here proves the whole process group | |
| // was killed. | |
| EXPECT_TRUE(pipe.WritersAreGone(absl::Seconds(5))) | |
| << "grandchild outlived Shutdown()"; | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, CooperativeTesteesGrandchildIsKilledToo) { | |
| InheritedPipe pipe; | |
| // Forks a grandchild that lives forever, answers one request, and exits on | |
| // EOF like a well-behaved testee, leaving the grandchild behind (a wrapper | |
| // script that backgrounds the real testee and exits on EOF does this). | |
| auto runner = MakeRunner("sleep 1000 & cat"); | |
| EXPECT_EQ(runner->RunTest("t", "abc"), "abc"); | |
| pipe.CloseWriteEnd(); | |
| ASSERT_FALSE(pipe.WritersAreGone(absl::ZeroDuration())) | |
| << "the testee should be holding the pipe open"; | |
| // The direct child exits at once, so the grace period is not waited out; | |
| // the grandchild is only gone if the process group was swept after that. | |
| EXPECT_LT(TimeDestruction(std::move(runner)), absl::Seconds(5)); | |
| EXPECT_TRUE(pipe.WritersAreGone(absl::Seconds(5))) | |
| << "grandchild outlived Shutdown()"; | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, CrashedTesteesGrandchildIsKilledToo) { | |
| InheritedPipe pipe; | |
| // Forks a grandchild that lives forever (with its stdout pointed away from | |
| // the pipe, so that the runner sees EOF as soon as the testee exits), then | |
| // consumes the request and exits with status 3 without answering. | |
| auto runner = | |
| MakeRunner("sleep 1000 >/dev/null & head -c 7 >/dev/null; exit 3"); | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t", "abc")); | |
| pipe.CloseWriteEnd(); | |
| EXPECT_THAT(response.runtime_error(), HasSubstr("exited with status=3")); | |
| EXPECT_FALSE(response.has_timeout_error()); | |
| // The crash path shut the testee down (it had exited already, so the grace | |
| // period wasn't waited out) and must have swept its process group as well. | |
| EXPECT_TRUE(pipe.WritersAreGone(absl::Seconds(5))) | |
| << "grandchild outlived the crash path"; | |
| EXPECT_TRUE(NoChildRemains()); | |
| runner.reset(); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, CrashedTesteeIsReapedAndRespawned) { | |
| // Consumes the 7-byte request, then exits with status 3 without answering. | |
| auto runner = MakeRunner("head -c 7 >/dev/null; exit 3"); | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t", "abc")); | |
| EXPECT_THAT(response.runtime_error(), HasSubstr("exited with status=3")); | |
| EXPECT_FALSE(response.has_timeout_error()); | |
| EXPECT_TRUE(NoChildRemains()) << "crash path must reap the testee"; | |
| // The next call respawns the (same) testee, which fails the same way. | |
| response = ParseResponse(runner->RunTest("t2", "abc")); | |
| EXPECT_THAT(response.runtime_error(), HasSubstr("exited with status=3")); | |
| // The crash path already shut the testee down, so the destructor has | |
| // nothing to wait for. | |
| EXPECT_LT(TimeDestruction(std::move(runner)), absl::Seconds(1)); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, TesteeReapedBehindTheRunnersBackIsNotKilledAgain) { | |
| // With SIGCHLD ignored, the kernel reaps the testee the moment it exits, so | |
| // the runner's own waitpid() finds no child. That must not be taken for a | |
| // testee that is still running: there is nothing to SIGKILL (the pid could | |
| // even have been reused), so the shutdown must neither escalate nor complain. | |
| ScopedAutoReap auto_reap; | |
| auto runner = MakeRunner("exec cat"); | |
| EXPECT_EQ(runner->RunTest("t", "abc"), "abc"); | |
| absl::ScopedMockLog log(absl::MockLogDefault::kIgnoreUnexpected); | |
| EXPECT_CALL(log, Log(absl::LogSeverity::kError, _, _)).Times(0); | |
| EXPECT_CALL(log, Log(absl::LogSeverity::kWarning, _, HasSubstr("SIGKILL"))) | |
| .Times(0); | |
| EXPECT_CALL(log, Log(absl::LogSeverity::kWarning, _, | |
| HasSubstr("reaped by something other than the runner"))); | |
| log.StartCapturingLogs(); | |
| EXPECT_LT(TimeDestruction(std::move(runner)), absl::Seconds(5)); | |
| } | |
| TEST(ForkPipeRunnerTest, TesteeReapedBehindTheRunnersBackHasUnknownStatus) { | |
| // The same situation on the failure path: the exit status of a testee the | |
| // kernel reaped is lost, and the failure has to say so rather than blame the | |
| // runner for a kill that never happened. | |
| ScopedAutoReap auto_reap; | |
| auto runner = MakeRunner("head -c 7 >/dev/null; exit 3"); | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t", "abc")); | |
| EXPECT_THAT(response.runtime_error(), HasSubstr("exit status unknown")); | |
| EXPECT_THAT(response.runtime_error(), Not(HasSubstr("killed by runner"))); | |
| EXPECT_FALSE(response.has_timeout_error()); | |
| } | |
| TEST(ForkPipeRunnerTest, SignaledTesteeIsReported) { | |
| // Consumes the request (so the runner's write cannot race the death of the | |
| // reader and raise SIGPIPE here), then kills itself. | |
| auto runner = MakeRunner("head -c 7 >/dev/null; kill -9 $$"); | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t", "abc")); | |
| EXPECT_THAT(response.runtime_error(), HasSubstr("killed by signal 9")); | |
| EXPECT_FALSE(response.has_timeout_error()); | |
| runner.reset(); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, TesteeClosingItsOutputIsKilledAndReported) { | |
| // Consumes the request, closes its stdout without answering and then lives | |
| // on: the runner sees EOF at once but has a live testee to shut down, so the | |
| // SIGKILL it sends must be reported as its own rather than as the testee's | |
| // death. | |
| auto runner = MakeRunner("head -c 7 >/dev/null; exec 1>&-; exec sleep 1000"); | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t", "abc")); | |
| EXPECT_EQ(response.runtime_error(), | |
| "child closed its output without responding (killed by runner)"); | |
| EXPECT_FALSE(response.has_timeout_error()); | |
| EXPECT_TRUE(NoChildRemains()) << "EOF path must reap the testee"; | |
| runner.reset(); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, HungTesteeTimesOutAndIsKilled) { | |
| // Consumes the request, then hangs without answering. It ignores SIGQUIT | |
| // (the ignore disposition survives the exec) and prints nothing in response | |
| // to it, which is the case in which the diagnostic read after the timeout | |
| // must be bounded, and it holds its stdout open so that read never sees EOF. | |
| constexpr absl::Duration kReadTimeout = absl::Milliseconds(200); | |
| auto runner = | |
| MakeRunner("trap '' QUIT; head -c 7 >/dev/null; exec sleep 1000", | |
| WithReadTimeout(kReadTimeout)); | |
| const absl::Time start = absl::Now(); | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t", "abc")); | |
| const absl::Duration elapsed = absl::Now() - start; | |
| EXPECT_THAT(response.timeout_error(), HasSubstr("child timed out")); | |
| EXPECT_THAT(response.timeout_error(), HasSubstr("killed by runner")); | |
| EXPECT_FALSE(response.has_runtime_error()); | |
| EXPECT_GE(elapsed, kReadTimeout); | |
| // Read timeout, plus the bounded SIGQUIT diagnostic read, plus the grace | |
| // period before SIGKILL; anything much longer means something blocked. | |
| EXPECT_LT(elapsed, absl::Seconds(15)); | |
| EXPECT_TRUE(NoChildRemains()) << "timeout path must reap the testee"; | |
| runner.reset(); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, HungTesteeKilledBySigquitIsReportedAsSuch) { | |
| // Hangs like the one above, but with SIGQUIT's default disposition, so the | |
| // SIGQUIT the runner sends on timeout kills it. The failure must say so | |
| // rather than claim the runner killed it: a testee that has failed gets a | |
| // moment to finish dying so that its own fate is what gets reported. | |
| constexpr absl::Duration kReadTimeout = absl::Milliseconds(200); | |
| auto runner = MakeRunner("head -c 7 >/dev/null; exec sleep 1000", | |
| WithReadTimeout(kReadTimeout)); | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t", "abc")); | |
| EXPECT_THAT(response.timeout_error(), HasSubstr("child timed out")); | |
| EXPECT_THAT(response.timeout_error(), HasSubstr("killed by signal 3")); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, SigquitOutputIsBounded) { | |
| // Answers SIGQUIT by flooding its stdout forever (a JVM's thread dump is | |
| // the benign version of this). The runner logs a bounded amount of it and | |
| // moves on to shutting the testee down, and closing its end of the pipe is | |
| // what ends the flood: `yes` dies of SIGPIPE, or of EPIPE if the test | |
| // process happens to ignore SIGPIPE (which the testee inherits), but either | |
| // way on its own, before the runner has to kill it. | |
| constexpr absl::Duration kReadTimeout = absl::Milliseconds(200); | |
| auto runner = MakeRunner( | |
| "head -c 7 >/dev/null; trap 'exec yes' QUIT; while :; do sleep 1; done", | |
| WithReadTimeout(kReadTimeout)); | |
| const absl::Time start = absl::Now(); | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t", "abc")); | |
| const absl::Duration elapsed = absl::Now() - start; | |
| EXPECT_THAT(response.timeout_error(), HasSubstr("child timed out")); | |
| EXPECT_THAT(response.timeout_error(), Not(HasSubstr("killed by runner"))); | |
| // Read timeout, up to a second for the shell to get to the trap, the | |
| // bounded diagnostic read and the grace period; an unbounded read would | |
| // never return. | |
| EXPECT_LT(elapsed, absl::Seconds(10)); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, TesteeHangingMidResponseTimesOutAndIsKilled) { | |
| // Consumes the request, announces an 8-byte response (the printf's first 4 | |
| // bytes are the little-endian length), sends 3 bytes of it and then hangs, | |
| // ignoring SIGQUIT and holding its stdout open, like | |
| // HungTesteeTimesOutAndIsKilled but with the hang inside the response body | |
| // rather than before the header. | |
| constexpr absl::Duration kReadTimeout = absl::Milliseconds(200); | |
| auto runner = MakeRunner( | |
| "trap '' QUIT; head -c 7 >/dev/null; printf '\\010\\000\\000\\000abc'; " | |
| "exec sleep 1000", | |
| WithReadTimeout(kReadTimeout)); | |
| const absl::Time start = absl::Now(); | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t", "abc")); | |
| const absl::Duration elapsed = absl::Now() - start; | |
| EXPECT_THAT(response.timeout_error(), HasSubstr("child timed out")); | |
| EXPECT_THAT(response.timeout_error(), HasSubstr("killed by runner")); | |
| EXPECT_FALSE(response.has_runtime_error()); | |
| EXPECT_GE(elapsed, kReadTimeout); | |
| EXPECT_LT(elapsed, absl::Seconds(15)); | |
| EXPECT_TRUE(NoChildRemains()) << "timeout path must reap the testee"; | |
| runner.reset(); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, TesteeExitingMidResponseIsReportedAndRespawned) { | |
| // Consumes the request, announces an 8-byte response, sends 3 bytes of it | |
| // and exits. | |
| auto runner = MakeRunner( | |
| "head -c 7 >/dev/null; printf '\\010\\000\\000\\000abc'; exit 0"); | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t", "abc")); | |
| EXPECT_THAT(response.runtime_error(), HasSubstr("exited with status=0")); | |
| EXPECT_FALSE(response.has_timeout_error()); | |
| EXPECT_TRUE(NoChildRemains()) << "crash path must reap the testee"; | |
| // The next call respawns the (same) testee, which fails the same way. | |
| response = ParseResponse(runner->RunTest("t2", "abc")); | |
| EXPECT_THAT(response.runtime_error(), HasSubstr("exited with status=0")); | |
| runner.reset(); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| // True iff some child of this process has exited within `timeout`. Only | |
| // peeks (WNOWAIT), so the child stays reapable by its runner. | |
| bool AChildHasExited(absl::Duration timeout) { | |
| const absl::Time deadline = absl::Now() + timeout; | |
| while (true) { | |
| siginfo_t info = {}; | |
| if (waitid(P_ALL, 0, &info, WEXITED | WNOHANG | WNOWAIT) == 0 && | |
| info.si_pid != 0) { | |
| return true; | |
| } | |
| if (absl::Now() >= deadline) return false; | |
| absl::SleepFor(absl::Milliseconds(10)); | |
| } | |
| } | |
| TEST(ForkPipeRunnerTest, TesteeExitingBetweenRequestsIsReportedAndRespawned) { | |
| // Answers one request in full and then exits instead of waiting for EOF. | |
| auto runner = MakeRunner( | |
| "head -c 7 >/dev/null; printf '\\003\\000\\000\\000abc'; exit 0"); | |
| EXPECT_EQ(runner->RunTest("t", "abc"), "abc"); | |
| // Once the testee is gone the pipe to it has no reader left, so the next | |
| // request cannot even be written: without special handling the write would | |
| // raise SIGPIPE and kill this process. | |
| ASSERT_TRUE(AChildHasExited(absl::Seconds(5))) | |
| << "the testee should have exited"; | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t2", "abc")); | |
| EXPECT_EQ(response.runtime_error(), | |
| "error writing to child (exited with status=0)"); | |
| EXPECT_FALSE(response.has_timeout_error()); | |
| EXPECT_TRUE(NoChildRemains()) << "write failure path must reap the testee"; | |
| // The next call respawns the testee, which answers its one request again. | |
| EXPECT_EQ(runner->RunTest("t3", "abc"), "abc"); | |
| runner.reset(); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| TEST(ForkPipeRunnerTest, NonexistentExecutableIsReported) { | |
| // The child's exec fails and it exits with status 1 at once, before or after | |
| // the request has been written; either way the failure is reported like any | |
| // other testee death, rather than crashing the runner. | |
| auto runner = std::make_unique<ForkPipeRunner>("/nonexistent/testee"); | |
| ::conformance::ConformanceResponse response = | |
| ParseResponse(runner->RunTest("t", "abc")); | |
| EXPECT_THAT(response.runtime_error(), HasSubstr("exited with status=1")); | |
| EXPECT_FALSE(response.has_timeout_error()); | |
| EXPECT_TRUE(NoChildRemains()); | |
| response = ParseResponse(runner->RunTest("t2", "abc")); | |
| EXPECT_THAT(response.runtime_error(), HasSubstr("exited with status=1")); | |
| runner.reset(); | |
| EXPECT_TRUE(NoChildRemains()); | |
| } | |
| } // namespace | |
| } // namespace conformance | |
| } // namespace protobuf | |
| } // namespace google | |
| #endif // !_WIN32 |