Add CRASHPAD_CHILD_TEST_MAIN() helper for multiprocess tests

Extends MultiprocessExec to support running functions registered via
CRASHPAD_CHILD_TEST_MAIN() as the main of a new child process.

Additionally, implements Fuchsia exit code checking, and adds a
CRASHPAD_CHILD_TEST_MAIN()-based test for that.

Bug: crashpad:196, crashpad:215
Change-Id: I49ce3f4d95a3b9823813e6df5a602cee2583bcf8
Reviewed-on: https://chromium-review.googlesource.com/879563
Commit-Queue: Scott Graham <scottmg@chromium.org>
Reviewed-by: Mark Mentovai <mark@chromium.org>
This commit is contained in:
Scott Graham 2018-01-25 14:47:28 -08:00 committed by Commit Bot
parent 26cd6138af
commit 48abd4a60f
10 changed files with 246 additions and 2 deletions

View File

@ -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])

View File

@ -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",

View File

@ -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)

View File

@ -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();

74
test/multiprocess_exec.cc Normal file
View File

@ -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 <map>
#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<std::string, int(*)()>* GetMultiprocessFunctionMap() {
static auto* map = new std::map<std::string, int(*)()>();
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<std::string> 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

View File

@ -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 executables 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<std::string>* 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();

View File

@ -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() {

View File

@ -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

View File

@ -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_;
}

View File

@ -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',