From ff26ea6db913ee4030d8ce22302e03fc47686989 Mon Sep 17 00:00:00 2001 From: Mark Mentovai Date: Tue, 26 Aug 2014 17:10:19 -0400 Subject: [PATCH] Refactor MachMultiprocess into a Multiprocess base class without any knowledge of Mach. MachMultiprocess builds on this new base class. TEST=util_test MachMultiprocess.*:Multiprocess.* R=rsesek@chromium.org Review URL: https://codereview.chromium.org/506143002 --- util/mac/process_reader_test.cc | 14 +- util/test/mac/mach_multiprocess.cc | 146 +++---------------- util/test/mac/mach_multiprocess.h | 103 ++++++-------- util/test/mac/mach_multiprocess_test.cc | 52 +------ util/test/multiprocess.cc | 180 ++++++++++++++++++++++++ util/test/multiprocess.h | 140 ++++++++++++++++++ util/test/multiprocess_test.cc | 84 +++++++++++ util/util.gyp | 3 + 8 files changed, 479 insertions(+), 243 deletions(-) create mode 100644 util/test/multiprocess.cc create mode 100644 util/test/multiprocess.h create mode 100644 util/test/multiprocess_test.cc diff --git a/util/mac/process_reader_test.cc b/util/mac/process_reader_test.cc index a5458929..67d90995 100644 --- a/util/mac/process_reader_test.cc +++ b/util/mac/process_reader_test.cc @@ -67,8 +67,8 @@ class ProcessReaderChild final : public MachMultiprocess { ~ProcessReaderChild() {} - protected: - void Parent() override { + private: + void MachMultiprocessParent() override { ProcessReader process_reader; ASSERT_TRUE(process_reader.Initialize(ChildTask())); @@ -100,7 +100,7 @@ class ProcessReaderChild final : public MachMultiprocess { ASSERT_EQ(1, rv) << ErrnoMessage("write"); } - void Child() override { + void MachMultiprocessChild() override { int write_fd = WritePipeFD(); mach_vm_address_t address = @@ -116,7 +116,6 @@ class ProcessReaderChild final : public MachMultiprocess { ASSERT_EQ(1, rv) << ErrnoMessage("read"); } - private: DISALLOW_COPY_AND_ASSIGN(ProcessReaderChild); }; @@ -434,8 +433,8 @@ class ProcessReaderThreadedChild final : public MachMultiprocess { ~ProcessReaderThreadedChild() {} - protected: - void Parent() override { + private: + void MachMultiprocessParent() override { ProcessReader process_reader; ASSERT_TRUE(process_reader.Initialize(ChildTask())); @@ -486,7 +485,7 @@ class ProcessReaderThreadedChild final : public MachMultiprocess { ASSERT_EQ(1, rv) << ErrnoMessage("write"); } - void Child() override { + void MachMultiprocessChild() override { TestThreadPool thread_pool; thread_pool.StartThreads(thread_count_); if (testing::Test::HasFatalFailure()) { @@ -550,7 +549,6 @@ class ProcessReaderThreadedChild final : public MachMultiprocess { ASSERT_EQ(1, rv) << ErrnoMessage("read"); } - private: size_t thread_count_; DISALLOW_COPY_AND_ASSIGN(ProcessReaderThreadedChild); diff --git a/util/test/mac/mach_multiprocess.cc b/util/test/mac/mach_multiprocess.cc index c5e749cf..d78b8600 100644 --- a/util/test/mac/mach_multiprocess.cc +++ b/util/test/mac/mach_multiprocess.cc @@ -17,35 +17,22 @@ #include #include #include -#include -#include -#include #include #include "base/auto_reset.h" -#include "base/files/scoped_file.h" #include "base/logging.h" #include "base/mac/scoped_mach_port.h" #include "base/memory/scoped_ptr.h" #include "base/rand_util.h" -#include "base/strings/stringprintf.h" #include "gtest/gtest.h" #include "util/mach/bootstrap.h" +#include "util/misc/scoped_forbid_return.h" #include "util/test/errors.h" #include "util/test/mac/mach_errors.h" namespace { -class ScopedNotReached { - public: - ScopedNotReached() {} - ~ScopedNotReached() { abort(); } - - private: - DISALLOW_COPY_AND_ASSIGN(ScopedNotReached); -}; - // The “hello” message contains a send right to the child process’ task port. struct SendHelloMessage : public mach_msg_base_t { mach_msg_port_descriptor_t port_descriptor; @@ -67,25 +54,11 @@ namespace internal { struct MachMultiprocessInfo { MachMultiprocessInfo() : service_name(), - pipe_c2p_read(-1), - pipe_c2p_write(-1), - pipe_p2c_read(-1), - pipe_p2c_write(-1), - child_pid(0), - read_pipe_fd(-1), - write_pipe_fd(-1), local_port(MACH_PORT_NULL), remote_port(MACH_PORT_NULL), child_task(MACH_PORT_NULL) {} std::string service_name; - base::ScopedFD pipe_c2p_read; // child to parent - base::ScopedFD pipe_c2p_write; // child to parent - base::ScopedFD pipe_p2c_read; // parent to child - base::ScopedFD pipe_p2c_write; // parent to child - pid_t child_pid; // valid only in parent - int read_pipe_fd; // pipe_c2p_read in parent, pipe_p2c_read in child - int write_pipe_fd; // pipe_p2c_write in parent, pipe_c2p_write in child base::mac::ScopedMachReceiveRight local_port; base::mac::ScopedMachSendRight remote_port; base::mac::ScopedMachSendRight child_task; // valid only in parent @@ -93,7 +66,7 @@ struct MachMultiprocessInfo { } // namespace internal -MachMultiprocess::MachMultiprocess() : info_(NULL) { +MachMultiprocess::MachMultiprocess() : Multiprocess(), info_(NULL) { } void MachMultiprocess::Run() { @@ -103,19 +76,17 @@ void MachMultiprocess::Run() { base::AutoReset reset_info(&info_, info.get()); - int pipe_fds_c2p[2]; - int rv = pipe(pipe_fds_c2p); - ASSERT_EQ(0, rv) << ErrnoMessage("pipe"); + return Multiprocess::Run(); +} - info_->pipe_c2p_read.reset(pipe_fds_c2p[0]); - info_->pipe_c2p_write.reset(pipe_fds_c2p[1]); +MachMultiprocess::~MachMultiprocess() { +} - int pipe_fds_p2c[2]; - rv = pipe(pipe_fds_p2c); - ASSERT_EQ(0, rv) << ErrnoMessage("pipe"); - - info_->pipe_p2c_read.reset(pipe_fds_p2c[0]); - info_->pipe_p2c_write.reset(pipe_fds_p2c[1]); +void MachMultiprocess::PreFork() { + Multiprocess::PreFork(); + if (testing::Test::HasFatalFailure()) { + return; + } // Set up the parent port and register it with the bootstrap server before // forking, so that it’s guaranteed to be there when the child attempts to @@ -131,65 +102,6 @@ void MachMultiprocess::Run() { ASSERT_EQ(BOOTSTRAP_SUCCESS, kr) << BootstrapErrorMessage(kr, "bootstrap_check_in"); info_->local_port.reset(local_port); - - pid_t pid = fork(); - ASSERT_GE(pid, 0) << ErrnoMessage("fork"); - - if (pid > 0) { - info_->child_pid = pid; - - RunParent(); - - // Waiting for the child happens here instead of in RunParent() because even - // if RunParent() returns early due to a gtest fatal assertion failure, the - // child should still be reaped. - - // This will make the parent hang up on the child as much as would be - // visible from the child’s perspective. The child’s side of the pipe will - // be broken, the child’s remote port will become a dead name, and an - // attempt by the child to look up the service will fail. If this weren’t - // done, the child might hang while waiting for a parent that has already - // triggered a fatal assertion failure to do something. - info.reset(); - info_ = NULL; - - int status; - pid_t wait_pid = waitpid(pid, &status, 0); - ASSERT_EQ(pid, wait_pid) << ErrnoMessage("waitpid"); - if (status != 0) { - std::string message; - if (WIFEXITED(status)) { - message = base::StringPrintf("Child exited with code %d", - WEXITSTATUS(status)); - } else if (WIFSIGNALED(status)) { - message = base::StringPrintf("Child terminated by signal %d (%s) %s", - WTERMSIG(status), - strsignal(WTERMSIG(status)), - WCOREDUMP(status) ? " (core dumped)" : ""); - } - ASSERT_EQ(0, status) << message; - } - } else { - RunChild(); - } -} - -MachMultiprocess::~MachMultiprocess() { -} - -pid_t MachMultiprocess::ChildPID() const { - EXPECT_NE(0, info_->child_pid); - return info_->child_pid; -} - -int MachMultiprocess::ReadPipeFD() const { - EXPECT_NE(-1, info_->read_pipe_fd); - return info_->read_pipe_fd; -} - -int MachMultiprocess::WritePipeFD() const { - EXPECT_NE(-1, info_->write_pipe_fd); - return info_->write_pipe_fd; } mach_port_t MachMultiprocess::LocalPort() const { @@ -207,13 +119,7 @@ mach_port_t MachMultiprocess::ChildTask() const { return info_->child_task; } -void MachMultiprocess::RunParent() { - // The parent uses the read end of c2p and the write end of p2c. - info_->pipe_c2p_write.reset(); - info_->read_pipe_fd = info_->pipe_c2p_read.get(); - info_->pipe_p2c_read.reset(); - info_->write_pipe_fd = info_->pipe_p2c_write.get(); - +void MachMultiprocess::MultiprocessParent() { ReceiveHelloMessage message = {}; kern_return_t kr = @@ -299,29 +205,18 @@ void MachMultiprocess::RunParent() { ASSERT_EQ(KERN_SUCCESS, kr) << MachErrorMessage(kr, "pid_for_task"); ASSERT_EQ(ChildPID(), mach_pid); - Parent(); + MachMultiprocessParent(); info_->remote_port.reset(); info_->local_port.reset(); - - info_->read_pipe_fd = -1; - info_->pipe_c2p_read.reset(); - info_->write_pipe_fd = -1; - info_->pipe_p2c_write.reset(); } -void MachMultiprocess::RunChild() { - ScopedNotReached must_not_leave_this_scope; +void MachMultiprocess::MultiprocessChild() { + ScopedForbidReturn forbid_return;; // local_port is not valid in the forked child process. ignore_result(info_->local_port.release()); - // The child uses the write end of c2p and the read end of p2c. - info_->pipe_c2p_read.reset(); - info_->write_pipe_fd = info_->pipe_c2p_write.get(); - info_->pipe_p2c_write.reset(); - info_->read_pipe_fd = info_->pipe_p2c_read.get(); - mach_port_t local_port; kern_return_t kr = mach_port_allocate( mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &local_port); @@ -360,22 +255,17 @@ void MachMultiprocess::RunChild() { MACH_PORT_NULL); ASSERT_EQ(MACH_MSG_SUCCESS, kr) << MachErrorMessage(kr, "mach_msg"); - Child(); + MachMultiprocessChild(); info_->remote_port.reset(); info_->local_port.reset(); - info_->write_pipe_fd = -1; - info_->pipe_c2p_write.reset(); - info_->read_pipe_fd = -1; - info_->pipe_p2c_read.reset(); - if (Test::HasFailure()) { - // Trigger the ScopedNotReached destructor. + // Trigger the ScopedForbidReturn destructor. return; } - exit(0); + forbid_return.Disarm(); } } // namespace test diff --git a/util/test/mac/mach_multiprocess.h b/util/test/mac/mach_multiprocess.h index d6835546..b949e652 100644 --- a/util/test/mac/mach_multiprocess.h +++ b/util/test/mac/mach_multiprocess.h @@ -19,6 +19,7 @@ #include #include "base/basictypes.h" +#include "util/test/multiprocess.h" namespace crashpad { namespace test { @@ -29,68 +30,26 @@ struct MachMultiprocessInfo; //! \brief Manages a Mach-aware multiprocess test. //! -//! These tests are `fork()`-based. The parent process has access to the child -//! process’ task port. The parent and child processes are able to communicate -//! via Mach IPC, and via a pair of POSIX pipes. +//! This is similar to the base Multiprocess test, but adds Mach features. The +//! parent process has access to the child process’ task port. The parent and +//! child processes are able to communicate via Mach IPC: each process has a +//! receive right to its “local port” and a send right to a “remote port”, and +//! messages sent to the remote port in one process can be received on the local +//! port in the partner process. //! //! Subclasses are expected to implement the parent and child by overriding the //! appropriate methods. -class MachMultiprocess { +class MachMultiprocess : public Multiprocess { public: MachMultiprocess(); - //! \brief Runs the test. - //! - //! This method establishes the proper testing environment and calls - //! RunParent() in the parent process and RunChild() in the child process. - //! - //! This method uses gtest assertions to validate the testing environment. If - //! the testing environment cannot be set up properly, it is possible that - //! Parent() or Child() will not be called. In the parent process, this method - //! also waits for the child process to exit after Parent() returns, and - //! verifies that it exited cleanly with gtest assertions. void Run(); protected: ~MachMultiprocess(); - //! \brief The subclass-provided parent routine. - //! - //! Test failures should be reported via gtest: `EXPECT_*()`, `ASSERT_*()`, - //! `FAIL()`, etc. - //! - //! This method must not use a `wait()`-family system call to wait for the - //! child process to exit, as this is handled by RunParent(). - //! - //! Subclasses must implement this method to define how the parent operates. - virtual void Parent() = 0; - - //! \brief The subclass-provided child routine. - //! - //! Test failures should be reported via gtest: `EXPECT_*()`, `ASSERT_*()`, - //! `FAIL()`, etc. - //! - //! Subclasses must implement this method to define how the child operates. - virtual void Child() = 0; - - //! \brief Returns the child process’ process ID. - //! - //! This method may only be called by the parent process. - pid_t ChildPID() const; - - //! \brief Returns the read pipe’s file descriptor. - //! - //! This method may be called by either the parent or the child process. - //! Anything written to the write pipe in the partner process will appear - //! on the this file descriptor in this process. - int ReadPipeFD() const; - - //! \brief Returns the write pipe’s file descriptor. - //! - //! This method may be called by either the parent or the child process. - //! Anything written to this file descriptor in this process will appear on - //! the read pipe in the partner process. - int WritePipeFD() const; + // Multiprocess: + virtual void PreFork() override; //! \brief Returns a receive right for the local port. //! @@ -112,21 +71,43 @@ class MachMultiprocess { mach_port_t ChildTask() const; private: + // Multiprocess: + //! \brief Runs the parent side of the test. //! - //! This method establishes the parent’s environment, performs the handshake - //! with the child, calls Parent(), and waits for the child process to exit. - //! Assuming that the environment can be set up correctly and the child exits - //! successfully, the test will pass. - void RunParent(); + //! This method establishes the parent’s environment and calls + //! MachMultiprocessParent(). + //! + //! Subclasses must override MachMultiprocessParent() instead of this method. + virtual void MultiprocessParent() override final; //! \brief Runs the child side of the test. //! - //! This method establishes the child’s environment, performs the handshake - //! with the parent, calls Child(), and exits cleanly. However, if any failure - //! (via fatal or nonfatal gtest assertion) is detected, the child will exit - //! with a failure status. - void RunChild(); + //! This method establishes the child’s environment and calls + //! MachMultiprocessChild(). If any failure (via fatal or nonfatal gtest + //! assertion) is detected, the child will exit with a failure status. + //! + //! Subclasses must override MachMultiprocessChild() instead of this method. + virtual void MultiprocessChild() override final; + + //! \brief The subclass-provided parent routine. + //! + //! Test failures should be reported via gtest: `EXPECT_*()`, `ASSERT_*()`, + //! `FAIL()`, etc. + //! + //! This method must not use a `wait()`-family system call to wait for the + //! child process to exit, as this is handled by the superclass. + //! + //! Subclasses must implement this method to define how the parent operates. + virtual void MachMultiprocessParent() = 0; + + //! \brief The subclass-provided child routine. + //! + //! Test failures should be reported via gtest: `EXPECT_*()`, `ASSERT_*()`, + //! `FAIL()`, etc. + //! + //! Subclasses must implement this method to define how the child operates. + virtual void MachMultiprocessChild() = 0; internal::MachMultiprocessInfo* info_; diff --git a/util/test/mac/mach_multiprocess_test.cc b/util/test/mac/mach_multiprocess_test.cc index a8639978..39295f65 100644 --- a/util/test/mac/mach_multiprocess_test.cc +++ b/util/test/mac/mach_multiprocess_test.cc @@ -18,8 +18,6 @@ #include "base/basictypes.h" #include "gtest/gtest.h" -#include "util/file/fd_io.h" -#include "util/test/errors.h" namespace { @@ -32,51 +30,13 @@ class TestMachMultiprocess final : public MachMultiprocess { ~TestMachMultiprocess() {} - protected: - // The base class will have already exercised the Mach ports for IPC and the - // child task port. Just make sure that the pipe is set up correctly and that - // ChildPID() works as expected. - virtual void Parent() override { - int read_fd = ReadPipeFD(); - char c; - ssize_t rv = ReadFD(read_fd, &c, 1); - ASSERT_EQ(1, rv) << ErrnoMessage("read"); - EXPECT_EQ('M', c); - - pid_t pid; - rv = ReadFD(read_fd, &pid, sizeof(pid)); - ASSERT_EQ(static_cast(sizeof(pid)), rv) << ErrnoMessage("read"); - EXPECT_EQ(pid, ChildPID()); - - int write_fd = WritePipeFD(); - c = 'm'; - rv = WriteFD(write_fd, &c, 1); - ASSERT_EQ(1, rv) << ErrnoMessage("write"); - - // The child will close its end of the pipe and exit. Make sure that the - // parent sees EOF. - rv = ReadFD(read_fd, &c, 1); - ASSERT_EQ(0, rv) << ErrnoMessage("read"); - } - - virtual void Child() override { - int write_fd = WritePipeFD(); - - char c = 'M'; - ssize_t rv = WriteFD(write_fd, &c, 1); - ASSERT_EQ(1, rv) << ErrnoMessage("write"); - - pid_t pid = getpid(); - rv = WriteFD(write_fd, &pid, sizeof(pid)); - ASSERT_EQ(static_cast(sizeof(pid)), rv) << ErrnoMessage("write"); - - int read_fd = ReadPipeFD(); - rv = ReadFD(read_fd, &c, 1); - ASSERT_EQ(1, rv) << ErrnoMessage("read"); - EXPECT_EQ('m', c); - } - private: + // MachMultiprocess will have already exercised the Mach ports for IPC and the + // child task port. + virtual void MachMultiprocessParent() override {} + + virtual void MachMultiprocessChild() override {} + DISALLOW_COPY_AND_ASSIGN(TestMachMultiprocess); }; diff --git a/util/test/multiprocess.cc b/util/test/multiprocess.cc new file mode 100644 index 00000000..34cbddc4 --- /dev/null +++ b/util/test/multiprocess.cc @@ -0,0 +1,180 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/test/multiprocess.h" + +#include +#include +#include + +#include + +#include "base/auto_reset.h" +#include "base/files/scoped_file.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/stringprintf.h" +#include "gtest/gtest.h" +#include "util/misc/scoped_forbid_return.h" +#include "util/test/errors.h" + +namespace crashpad { +namespace test { + +using namespace testing; + +namespace internal { + +struct MultiprocessInfo { + MultiprocessInfo() + : pipe_c2p_read(-1), + pipe_c2p_write(-1), + pipe_p2c_read(-1), + pipe_p2c_write(-1), + child_pid(0) {} + + base::ScopedFD pipe_c2p_read; // child to parent + base::ScopedFD pipe_c2p_write; // child to parent + base::ScopedFD pipe_p2c_read; // parent to child + base::ScopedFD pipe_p2c_write; // parent to child + pid_t child_pid; // valid only in parent +}; + +} // namespace internal + +Multiprocess::Multiprocess() : info_(NULL) { +} + +void Multiprocess::Run() { + ASSERT_EQ(NULL, info_); + scoped_ptr info(new internal::MultiprocessInfo); + base::AutoReset reset_info(&info_, info.get()); + + PreFork(); + if (testing::Test::HasFatalFailure()) { + return; + } + + pid_t pid = fork(); + ASSERT_GE(pid, 0) << ErrnoMessage("fork"); + + if (pid > 0) { + info_->child_pid = pid; + + RunParent(); + + // Waiting for the child happens here instead of in RunParent() because even + // if RunParent() returns early due to a gtest fatal assertion failure, the + // child should still be reaped. + + // This will make the parent hang up on the child as much as would be + // visible from the child’s perspective. The child’s side of the pipe will + // be broken, the child’s remote port will become a dead name, and an + // attempt by the child to look up the service will fail. If this weren’t + // done, the child might hang while waiting for a parent that has already + // triggered a fatal assertion failure to do something. + info.reset(); + info_ = NULL; + + int status; + pid_t wait_pid = waitpid(pid, &status, 0); + ASSERT_EQ(pid, wait_pid) << ErrnoMessage("waitpid"); + if (status != 0) { + std::string message; + if (WIFEXITED(status)) { + message = base::StringPrintf("Child exited with code %d", + WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + message = base::StringPrintf("Child terminated by signal %d (%s) %s", + WTERMSIG(status), + strsignal(WTERMSIG(status)), + WCOREDUMP(status) ? " (core dumped)" : ""); + } + ASSERT_EQ(0, status) << message; + } + } else { + RunChild(); + } +} + +Multiprocess::~Multiprocess() { +} + +void Multiprocess::PreFork() { + int pipe_fds_c2p[2]; + int rv = pipe(pipe_fds_c2p); + ASSERT_EQ(0, rv) << ErrnoMessage("pipe"); + + info_->pipe_c2p_read.reset(pipe_fds_c2p[0]); + info_->pipe_c2p_write.reset(pipe_fds_c2p[1]); + + int pipe_fds_p2c[2]; + rv = pipe(pipe_fds_p2c); + ASSERT_EQ(0, rv) << ErrnoMessage("pipe"); + + info_->pipe_p2c_read.reset(pipe_fds_p2c[0]); + info_->pipe_p2c_write.reset(pipe_fds_p2c[1]); +} + +pid_t Multiprocess::ChildPID() const { + EXPECT_NE(0, info_->child_pid); + return info_->child_pid; +} + +int Multiprocess::ReadPipeFD() const { + int fd = info_->child_pid ? info_->pipe_c2p_read.get() + : info_->pipe_p2c_read.get(); + EXPECT_NE(-1, fd); + return fd; +} + +int Multiprocess::WritePipeFD() const { + int fd = info_->child_pid ? info_->pipe_p2c_write.get() + : info_->pipe_c2p_write.get(); + EXPECT_NE(-1, fd); + return fd; +} + +void Multiprocess::RunParent() { + // The parent uses the read end of c2p and the write end of p2c. + info_->pipe_c2p_write.reset(); + info_->pipe_p2c_read.reset(); + + MultiprocessParent(); + + info_->pipe_c2p_read.reset(); + info_->pipe_p2c_write.reset(); +} + +void Multiprocess::RunChild() { + ScopedForbidReturn forbid_return; + + // The child uses the write end of c2p and the read end of p2c. + info_->pipe_c2p_read.reset(); + info_->pipe_p2c_write.reset(); + + MultiprocessChild(); + + info_->pipe_c2p_write.reset(); + info_->pipe_p2c_read.reset(); + + if (Test::HasFailure()) { + // Trigger the ScopedForbidReturn destructor. + return; + } + + exit(0); +} + +} // namespace test +} // namespace crashpad diff --git a/util/test/multiprocess.h b/util/test/multiprocess.h new file mode 100644 index 00000000..80ee8415 --- /dev/null +++ b/util/test/multiprocess.h @@ -0,0 +1,140 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CRASHPAD_UTIL_TEST_MULTIPROCESS_H_ +#define CRASHPAD_UTIL_TEST_MULTIPROCESS_H_ + +#include + +#include "base/basictypes.h" + +namespace crashpad { +namespace test { + +namespace internal { +struct MultiprocessInfo; +}; + +//! \brief Manages a multiprocess test. +//! +//! These tests are `fork()`-based. The parent and child processes are able to +//! communicate via a pair of POSIX pipes. +//! +//! Subclasses are expected to implement the parent and child by overriding the +//! appropriate methods. +class Multiprocess { + public: + Multiprocess(); + + //! \brief Runs the test. + //! + //! This method establishes the proper testing environment by calling + //! PreFork(), then calls `fork()`. In the parent process, it calls + //! RunParent(), and in the child process, it calls RunChild(). + //! + //! This method uses gtest assertions to validate the testing environment. If + //! the testing environment cannot be set up properly, it is possible that + //! MultiprocessParent() or MultiprocessChild() will not be called. In the + //! parent process, this method also waits for the child process to exit after + //! MultiprocessParent() returns, and verifies that it exited cleanly without + //! raising any gtest assertions. + void Run(); + + protected: + ~Multiprocess(); + + //! \brief Establishes the proper testing environment prior to forking. + //! + //! Subclasses that solely implement a test should not need to override this + //! method. Subclasses that do not implement tests but instead implement + //! additional testing features on top of this class may override this method + //! provided that they call the superclass’ implementation first as follows: + //! + //! \code + //! virtual void PreFork() override { + //! Multiprocess::PreFork(); + //! if (testing::Test::HasFatalAssertions()) { + //! return; + //! } + //! + //! // Place subclass-specific pre-fork code here. + //! } + //! \endcode + //! + //! Subclass implementations may signal failure by raising their own fatal + //! gtest assertions. + virtual void PreFork(); + + //! \brief Returns the child process’ process ID. + //! + //! This method may only be called by the parent process. + pid_t ChildPID() const; + + //! \brief Returns the read pipe’s file descriptor. + //! + //! This method may be called by either the parent or the child process. + //! Anything written to the write pipe in the partner process will appear + //! on the this file descriptor in this process. + int ReadPipeFD() const; + + //! \brief Returns the write pipe’s file descriptor. + //! + //! This method may be called by either the parent or the child process. + //! Anything written to this file descriptor in this process will appear on + //! the read pipe in the partner process. + int WritePipeFD() const; + + private: + //! \brief Runs the parent side of the test. + //! + //! This method establishes the parent’s environment and calls + //! MultiprocessParent(). + void RunParent(); + + //! \brief Runs the child side of the test. + //! + //! This method establishes the child’s environment, calls + //! MultiprocessChild(), and exits cleanly. However, if any failure (via fatal + //! or nonfatal gtest assertion) is detected, the child will exit with a + //! failure status. + void RunChild(); + + //! \brief The subclass-provided parent routine. + //! + //! Test failures should be reported via gtest: `EXPECT_*()`, `ASSERT_*()`, + //! `FAIL()`, etc. + //! + //! This method must not use a `wait()`-family system call to wait for the + //! child process to exit, as this is handled by this class. + //! + //! Subclasses must implement this method to define how the parent operates. + virtual void MultiprocessParent() = 0; + + //! \brief The subclass-provided child routine. + //! + //! Test failures should be reported via gtest: `EXPECT_*()`, `ASSERT_*()`, + //! `FAIL()`, etc. + //! + //! Subclasses must implement this method to define how the child operates. + virtual void MultiprocessChild() = 0; + + internal::MultiprocessInfo* info_; + + DISALLOW_COPY_AND_ASSIGN(Multiprocess); +}; + +} // namespace test +} // namespace crashpad + +#endif // CRASHPAD_UTIL_TEST_MULTIPROCESS_H_ diff --git a/util/test/multiprocess_test.cc b/util/test/multiprocess_test.cc new file mode 100644 index 00000000..53b60ef0 --- /dev/null +++ b/util/test/multiprocess_test.cc @@ -0,0 +1,84 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/test/multiprocess.h" + +#include + +#include "base/basictypes.h" +#include "gtest/gtest.h" +#include "util/file/fd_io.h" +#include "util/test/errors.h" + +namespace { + +using namespace crashpad; +using namespace crashpad::test; + +class TestMultiprocess final : public Multiprocess { + public: + TestMultiprocess() : Multiprocess() {} + + ~TestMultiprocess() {} + + private: + virtual void MultiprocessParent() override { + int read_fd = ReadPipeFD(); + char c; + ssize_t rv = ReadFD(read_fd, &c, 1); + ASSERT_EQ(1, rv) << ErrnoMessage("read"); + EXPECT_EQ('M', c); + + pid_t pid; + rv = ReadFD(read_fd, &pid, sizeof(pid)); + ASSERT_EQ(static_cast(sizeof(pid)), rv) << ErrnoMessage("read"); + EXPECT_EQ(pid, ChildPID()); + + int write_fd = WritePipeFD(); + c = 'm'; + rv = WriteFD(write_fd, &c, 1); + ASSERT_EQ(1, rv) << ErrnoMessage("write"); + + // The child will close its end of the pipe and exit. Make sure that the + // parent sees EOF. + rv = ReadFD(read_fd, &c, 1); + ASSERT_EQ(0, rv) << ErrnoMessage("read"); + } + + virtual void MultiprocessChild() override { + int write_fd = WritePipeFD(); + + char c = 'M'; + ssize_t rv = WriteFD(write_fd, &c, 1); + ASSERT_EQ(1, rv) << ErrnoMessage("write"); + + pid_t pid = getpid(); + rv = WriteFD(write_fd, &pid, sizeof(pid)); + ASSERT_EQ(static_cast(sizeof(pid)), rv) << ErrnoMessage("write"); + + int read_fd = ReadPipeFD(); + rv = ReadFD(read_fd, &c, 1); + ASSERT_EQ(1, rv) << ErrnoMessage("read"); + EXPECT_EQ('m', c); + } + + DISALLOW_COPY_AND_ASSIGN(TestMultiprocess); +}; + +TEST(Multiprocess, Multiprocess) { + TestMultiprocess multiprocess; + multiprocess.Run(); +} + +} // namespace diff --git a/util/util.gyp b/util/util.gyp index 8e9dd363..0cbc0674 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -98,6 +98,8 @@ 'test/mac/mach_errors.h', 'test/mac/mach_multiprocess.cc', 'test/mac/mach_multiprocess.h', + 'test/multiprocess.cc', + 'test/multiprocess.h', ], }, { @@ -132,6 +134,7 @@ 'posix/process_util_test.cc', 'stdlib/strlcpy_test.cc', 'test/mac/mach_multiprocess_test.cc', + 'test/multiprocess_test.cc', ], }, ],