diff --git a/build/run_tests.py b/build/run_tests.py index 19833a8e..43e49f2c 100755 --- a/build/run_tests.py +++ b/build/run_tests.py @@ -407,7 +407,8 @@ def _RunOnFuchsiaTarget(binary_dir, test, device_name, extra_command_line): done_message = 'TERMINATED: ' + unique_id namespace_command = [ - 'namespace', '/pkg=' + staging_root, '/tmp=' + tmp_root, '--', + 'namespace', '/pkg=' + staging_root, '/tmp=' + tmp_root, + '--replace-child-argv0=/pkg/bin/' + test, '--', staging_root + '/bin/' + test] + extra_command_line netruncmd(namespace_command, ['echo', done_message]) diff --git a/test/BUILD.gn b/test/BUILD.gn index 4b1eff24..f7541749 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -32,6 +32,7 @@ static_library("test") { "main_arguments.cc", "main_arguments.h", "multiprocess.h", + "multiprocess_exec.cc", "multiprocess_exec.h", "process_type.cc", "process_type.h", diff --git a/test/gtest_main.cc b/test/gtest_main.cc index 5a3d7996..ebdbeb9d 100644 --- a/test/gtest_main.cc +++ b/test/gtest_main.cc @@ -16,6 +16,7 @@ #include "gtest/gtest.h" #include "test/gtest_disabled.h" #include "test/main_arguments.h" +#include "test/multiprocess_exec.h" #if defined(CRASHPAD_TEST_LAUNCHER_GMOCK) #include "gmock/gmock.h" @@ -31,11 +32,34 @@ #include "base/test/test_suite.h" #endif // CRASHPAD_IS_IN_CHROMIUM +namespace { + +bool GetChildTestFunctionName(std::string* child_func_name) { + constexpr size_t arg_length = + sizeof(crashpad::test::internal::kChildTestFunction) - 1; + for (const auto& it : crashpad::test::GetMainArguments()) { + if (it.compare( + 0, arg_length, crashpad::test::internal::kChildTestFunction) == 0) { + *child_func_name = it.substr(arg_length); + return true; + } + } + return false; +} + +} // namespace + int main(int argc, char* argv[]) { crashpad::test::InitializeMainArguments(argc, argv); testing::AddGlobalTestEnvironment( crashpad::test::DisabledTestGtestEnvironment::Get()); + std::string child_func_name; + if (GetChildTestFunctionName(&child_func_name)) { + return crashpad::test::internal::CheckedInvokeMultiprocessChild( + child_func_name); + } + #if defined(CRASHPAD_IS_IN_CHROMIUM) #if defined(OS_WIN) diff --git a/test/multiprocess.h b/test/multiprocess.h index 35d18b2f..1d3ee9b2 100644 --- a/test/multiprocess.h +++ b/test/multiprocess.h @@ -49,11 +49,13 @@ class Multiprocess { //! that call `exit()` or `_exit()`. kTerminationNormal = false, +#if !defined(OS_FUCHSIA) // There are no signals on Fuchsia. //! \brief The child terminated by signal. //! //! Signal termination happens as a result of a crash, a call to `abort()`, //! assertion failure (including gtest assertions), etc. kTerminationSignal, +#endif // !defined(OS_FUCHSIA) }; Multiprocess(); diff --git a/test/multiprocess_exec.cc b/test/multiprocess_exec.cc new file mode 100644 index 00000000..fb332417 --- /dev/null +++ b/test/multiprocess_exec.cc @@ -0,0 +1,74 @@ +// Copyright 2018 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 "test/multiprocess_exec.h" + +#include + +#include "base/logging.h" +#include "base/strings/utf_string_conversions.h" +#include "test/main_arguments.h" +#include "util/stdlib/map_insert.h" +#include "test/test_paths.h" + +namespace crashpad { +namespace test { + +namespace internal { + +namespace { + +std::map* GetMultiprocessFunctionMap() { + static auto* map = new std::map(); + return map; +} + +} // namespace + +AppendMultiprocessTest::AppendMultiprocessTest(const std::string& test_name, + int (*main_function_pointer)()) { + CHECK(MapInsertOrReplace( + GetMultiprocessFunctionMap(), test_name, main_function_pointer, nullptr)) + << test_name << " already registered"; +} + +int CheckedInvokeMultiprocessChild(const std::string& test_name) { + const auto* functions = internal::GetMultiprocessFunctionMap(); + auto it = functions->find(test_name); + CHECK(it != functions->end()) + << "child main " << test_name << " not registered"; + return (*it->second)(); +} + +} // namespace internal + +void MultiprocessExec::SetChildTestMainFunction( + const std::string& function_name) { + std::vector rest(GetMainArguments().begin() + 1, + GetMainArguments().end()); + rest.push_back(internal::kChildTestFunction + function_name); + +#if defined(OS_WIN) + // Instead of using argv[0] on Windows, use the actual binary name. This is + // necessary because if originally the test isn't run with ".exe" on the + // command line, then argv[0] also won't include ".exe". This argument is used + // as the lpApplicationName argument to CreateProcess(), and so will fail. + SetChildCommand(TestPaths::Executable(), &rest); +#else + SetChildCommand(base::FilePath(GetMainArguments()[0]), &rest); +#endif +} + +} // namespace test +} // namespace crashpad diff --git a/test/multiprocess_exec.h b/test/multiprocess_exec.h index 258a3f8e..58b6808e 100644 --- a/test/multiprocess_exec.h +++ b/test/multiprocess_exec.h @@ -23,9 +23,57 @@ #include "build/build_config.h" #include "test/multiprocess.h" +//! \file + namespace crashpad { namespace test { +namespace internal { + +//! \brief Command line argument used to indicate that a child test function +//! should be run. +constexpr char kChildTestFunction[] = "--child-test-function="; + + +//! \brief Helper class used by CRASHPAD_CHILD_TEST_MAIN() to insert a child +//! function into the global mapping. +class AppendMultiprocessTest { + public: + AppendMultiprocessTest(const std::string& test_name, + int (*main_function_pointer)()); +}; + +//! \brief Used to run a child test function by name, registered by +//! CRASHPAD_CHILD_TEST_MAIN(). +//! +//! \return The exit code of the child process after running the function named +//! by \a test_name. Aborts with a CHECK() if \a test_name wasn't +//! registered. +int CheckedInvokeMultiprocessChild(const std::string& test_name); + +} // namespace internal + +//! \brief Registers a function that can be invoked as a child process by +//! MultiprocessExec. +//! +//! Used as: +//! +//! \code +//! CRASHPAD_CHILD_TEST_MAIN(MyChildTestBody) { +//! ... child body ... +//! } +//! \endcode +//! +//! In the main (parent) test body, this function can be run in a child process +//! via MultiprocessExec::SetChildTestMainFunction(). +#define CRASHPAD_CHILD_TEST_MAIN(test_main) \ + int test_main(); \ + namespace { \ + ::crashpad::test::internal::AppendMultiprocessTest \ + AddMultiprocessTest##_##test_main(#test_main, (test_main)); \ + } /* namespace */ \ + int test_main() + //! \brief Manages an `exec()`-based multiprocess test. //! //! These tests are based on `fork()` and `exec()`. The parent process is able @@ -44,14 +92,31 @@ class MultiprocessExec : public Multiprocess { //! //! This method must be called before the test can be Run(). //! + //! This method is useful when a custom executable is required for the child + //! binary, however, SetChildTestMainFunction() should generally be preferred. + //! //! \param[in] command The executable’s pathname. //! \param[in] arguments The command-line arguments to pass to the child //! process in its `argv[]` vector. This vector must begin at `argv[1]`, //! as \a command is implicitly used as `argv[0]`. This argument may be //! `nullptr` if no command-line arguments are to be passed. + //! + //! \sa SetChildTestMainFunction void SetChildCommand(const base::FilePath& command, const std::vector* arguments); + //! \brief Calls SetChildCommand() to run a child test main function + //! registered with CRASHPAD_CHILD_TEST_MAIN(). + //! + //! This uses the same launch mechanism as SetChildCommand(), but coordinates + //! with test/gtest_main.cc to allow for simple registration of a child + //! processes' entry point via the helper macro, rather than needing to + //! create a separate build target. + //! + //! \param[in] function_name The name of the function as passed to + //! CRASHPAD_CHILD_TEST_MAIN(). + void SetChildTestMainFunction(const std::string& function_name); + protected: ~MultiprocessExec(); diff --git a/test/multiprocess_exec_fuchsia.cc b/test/multiprocess_exec_fuchsia.cc index 324e7eb0..2f788028 100644 --- a/test/multiprocess_exec_fuchsia.cc +++ b/test/multiprocess_exec_fuchsia.cc @@ -51,13 +51,39 @@ void Multiprocess::Run() { // And then run the parent actions in this process. RunParent(); - // Reap the child. + // Wait until the child exits. zx_signals_t signals; ASSERT_EQ( zx_object_wait_one( info_->child.get(), ZX_TASK_TERMINATED, ZX_TIME_INFINITE, &signals), ZX_OK); ASSERT_EQ(signals, ZX_TASK_TERMINATED); + + // Get the child's exit code. + zx_info_process_t proc_info; + zx_status_t status = zx_object_get_info(info_->child.get(), + ZX_INFO_PROCESS, + &proc_info, + sizeof(proc_info), + nullptr, + nullptr); + if (status != ZX_OK) { + ZX_LOG(ERROR, status) << "zx_object_get_info"; + ADD_FAILURE() << "Unable to get exit code of child"; + } else { + if (code_ != proc_info.return_code) { + ADD_FAILURE() << "Child exited with code " << proc_info.return_code + << ", expected exit with code " << code_; + } + } +} + +void Multiprocess::SetExpectedChildTermination(TerminationReason reason, + int code) { + EXPECT_EQ(info_, nullptr) + << "SetExpectedChildTermination() must be called before Run()"; + reason_ = reason; + code_ = code; } Multiprocess::~Multiprocess() { diff --git a/test/multiprocess_exec_test.cc b/test/multiprocess_exec_test.cc index b8a5bc19..0104b0f3 100644 --- a/test/multiprocess_exec_test.cc +++ b/test/multiprocess_exec_test.cc @@ -14,6 +14,7 @@ #include "test/multiprocess_exec.h" +#include "base/logging.h" #include "base/macros.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" @@ -56,6 +57,47 @@ TEST(MultiprocessExec, MultiprocessExec) { multiprocess_exec.Run(); } + +CRASHPAD_CHILD_TEST_MAIN(SimpleMultiprocess) { + char c; + CheckedReadFileExactly(StdioFileHandle(StdioStream::kStandardInput), &c, 1); + LOG_IF(FATAL, c != 'z'); + + c = 'Z'; + CheckedWriteFile(StdioFileHandle(StdioStream::kStandardOutput), &c, 1); + return 0; +} + +TEST(MultiprocessExec, MultiprocessExecSimpleChild) { + TestMultiprocessExec exec; + exec.SetChildTestMainFunction("SimpleMultiprocess"); + exec.Run(); +}; + + +CRASHPAD_CHILD_TEST_MAIN(SimpleMultiprocessReturnsNonZero) { + return 123; +} + +class TestMultiprocessExecEmpty final : public MultiprocessExec { + public: + TestMultiprocessExecEmpty() = default; + ~TestMultiprocessExecEmpty() = default; + + private: + void MultiprocessParent() override {} + + DISALLOW_COPY_AND_ASSIGN(TestMultiprocessExecEmpty); +}; + +TEST(MultiprocessExec, MultiprocessExecSimpleChildReturnsNonZero) { + TestMultiprocessExecEmpty exec; + exec.SetChildTestMainFunction("SimpleMultiprocessReturnsNonZero"); + exec.SetExpectedChildTermination( + Multiprocess::TerminationReason::kTerminationNormal, 123); + exec.Run(); +}; + } // namespace } // namespace test } // namespace crashpad diff --git a/test/multiprocess_exec_win.cc b/test/multiprocess_exec_win.cc index 98fd0ac1..f5190c97 100644 --- a/test/multiprocess_exec_win.cc +++ b/test/multiprocess_exec_win.cc @@ -57,6 +57,14 @@ void Multiprocess::Run() { CloseHandle(info_->process_info.hProcess); } +void Multiprocess::SetExpectedChildTermination(TerminationReason reason, + int code) { + EXPECT_EQ(info_, nullptr) + << "SetExpectedChildTermination() must be called before Run()"; + reason_ = reason; + code_ = code; +} + Multiprocess::~Multiprocess() { delete info_; } diff --git a/test/test.gyp b/test/test.gyp index 699d984f..a3721efd 100644 --- a/test/test.gyp +++ b/test/test.gyp @@ -58,6 +58,7 @@ 'main_arguments.cc', 'main_arguments.h', 'multiprocess.h', + 'multiprocess_exec.cc', 'multiprocess_exec.h', 'multiprocess_exec_posix.cc', 'multiprocess_exec_win.cc',