blob: 6f0842317eba511cf10003d085947152460357b0 [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IPCZ_SRC_TEST_MULTINODE_TEST_H_
#define IPCZ_SRC_TEST_MULTINODE_TEST_H_
#include <memory>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
#include "ipcz/ipcz.h"
#include "test/test_base.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
#include "third_party/abseil-cpp/absl/base/macros.h"
#include "third_party/abseil-cpp/absl/types/span.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "third_party/ipcz/src/test_buildflags.h"
#include "util/ref_counted.h"
#if BUILDFLAG(ENABLE_IPCZ_MULTIPROCESS_TESTS)
#include "test/test_child_launcher.h"
#endif
namespace ipcz::test {
class TestNode;
template <typename TestNodeType>
class MultinodeTest;
// Selects which driver will be used by test nodes. Interconnecting nodes must
// always use the same driver.
//
// Multinode tests are parameterized over these modes to provide coverage of
// various interesting constraints encountered in production. Some platforms
// require driver objects to be relayed through a broker. Some environments
// prevent nodes from allocating their own shared memory regions.
//
// Incongruity between synchronous and asynchronous test failures generally
// indicates race conditions within ipcz, but many bugs will cause failures in
// all driver modes. The synchronous version is generally easier to debug in
// such cases.
enum class DriverMode {
// Use the synchronous, single-process reference driver. This driver does not
// create any background threads and all ipcz operations (e.g. message
// delivery, portal transfer, proxy elimination, etc) complete synchronously
// from end-to-end. Each test node runs its test body on a dedicated thread
// within the test process.
kSync,
// Use the asynchronous single-process reference driver. Transport messages
// are received asynchronously, similar to how most production drivers are
// likely to operate in practice. Such asynchrony gives rise to
// non-determinism throughout ipcz proper and provides good coverage of
// potential race conditions.
//
// As with the kSync driver, each test node runs its test body on a dedicated
// thread within the test process.
kAsync,
// Use the same driver as kAsync, but non-broker nodes are forced to delegate
// shared memory allocation to their broker. This simulates the production
// constraints of some sandbox environments and exercises additional
// asynchrony in ipcz proper.
kAsyncDelegatedAlloc,
// Use the same driver as kAsync, but driver objects cannot be transmitted
// directly between non-brokers and must instead be relayed by a broker. This
// simulates the production constraints of some sandbox environments and
// exercises additional asynchrony in ipcz proper.
kAsyncObjectBrokering,
// Use the same driver as kAsync, imposing the additional constraints of both
// kAsyncDelegatedAlloc and kAsyncObjectBrokering as described above.
kAsyncObjectBrokeringAndDelegatedAlloc,
#if BUILDFLAG(ENABLE_IPCZ_MULTIPROCESS_TESTS)
// Use a multiprocess-capable driver (Linux only for now), with each test node
// running in its own isolated child process.
kMultiprocess,
#endif
};
namespace internal {
using TestNodeFactory = std::unique_ptr<TestNode> (*)();
template <typename TestNodeType>
std::unique_ptr<TestNode> MakeTestNode() {
return std::make_unique<TestNodeType>();
}
// Type used to package metadata about a MULTINODE_TEST_NODE() invocation.
struct TestNodeDetails {
const std::string_view name;
const TestNodeFactory factory;
};
template <typename T>
static constexpr bool IsValidTestNodeType = std::is_base_of_v<TestNode, T>;
} // namespace internal
// Base class to support tests which exercise behavior across multiple ipcz
// nodes. These may be single-process on a synchronous driver, single-process on
// an asynchronous (e.g. multiprocess) driver, or fully multiprocess.
//
// This class provides convenience methods for creating and connecting nodes
// in various useful configurations. Note that it does NOT inherit from GTest's
// Test class, as multiple instances may run in parallel for a single test, and
// GTest's Test class is not compatible with that behavior.
//
// Instead, while MULTINODE_TEST_NODE() invocations should be based directly on
// TestNode or a derivative thereof. TEST_P() invocations for multinode tests
// should be based on derivatives of MultinodeTest<T> (see below this class),
// where T itself is a TestNode or some derivative thereof.
//
// This arrangement allows the main test body and its related
// MULTINODE_TEST_NODE() invocations to be based on the same essential type,
// making multinode tests easier to read and write.
class TestNode : public internal::TestBase {
public:
// Exposes interaction with one node spawned by another.
class TestNodeController : public RefCounted {
public:
// Blocks until the spawned node has terminated. Returns true if the node
// executed and terminated cleanly, or false if it encountered at least one
// test expectation failure while running.
virtual bool WaitForShutdown() = 0;
};
virtual ~TestNode();
// Handle to this node.
IpczHandle node() const { return node_; }
// Handle to this node's broker-facing transport, if and only if
// ConnectToBroker() hasn't been called yet.
IpczDriverHandle transport() const { return transport_; }
// Releases transport() to the caller. After calling this, it is no longer
// valid to call either transport() or ConnectToBroker(), and this fixture
// will not automatically close the transport on destruction.
IpczDriverHandle ReleaseTransport() {
return std::exchange(transport_, IPCZ_INVALID_DRIVER_HANDLE);
}
// The driver currently in use. Selected by test parameter.
const IpczDriver& GetDriver() const;
// One-time initialization. Called internally during test setup. Should never
// be called by individual test code.
void Initialize(DriverMode driver_mode,
IpczCreateNodeFlags create_node_flags);
// May be called at most once by the TestNode body, to connect initial
// `portals` to the broker.
void ConnectToBroker(absl::Span<IpczHandle> portals);
// Shorthand for the above, for the common case with only one initial portal.
IpczHandle ConnectToBroker();
// Opens a new portal pair on this node.
std::pair<IpczHandle, IpczHandle> OpenPortals();
// Spawns a new test node of TestNodeType and populates `portals` with a set
// of initial portals connected to the node, via a new transport.
template <typename TestNodeType>
Ref<TestNodeController> SpawnTestNode(absl::Span<IpczHandle> portals) {
return SpawnTestNodeImpl(node_, TestNodeType::kDetails, portals);
}
// Shorthand for the above, for the common case with only one initial portal
// and no need for the test body to retain a controller for the node.
template <typename TestNodeType>
IpczHandle SpawnTestNode() {
IpczHandle portal;
SpawnTestNode<TestNodeType>({&portal, 1});
return portal;
}
// Spawns a new test node of TestNodeType, giving it `transport` to use for
// its broker connection. The caller is resposible for the other end of that
// connection.
template <typename TestNodeType>
Ref<TestNodeController> SpawnTestNode(IpczDriverHandle transport) {
return SpawnTestNodeImpl(node_, TestNodeType::kDetails, transport);
}
// Forcibly closes this Node, severing all links to other nodes and implicitly
// disconnecting any portals which relied on those links.
void CloseThisNode();
// The TestNode body provided by a MULTINODE_TEST_NODE() invocation. For main
// test definitions via TEST_P() with a MultinodeTest<T> fixture, this is
// unused in favor of TestBody().
virtual void NodeBody() {}
// Creates a pair of transports appropriate for connecting this (broker or
// non-broker) node to another non-broker node. Most tests should not use this
// directly, but should instead connect to other nodes using the more
// convenient helpers ConnectToBroker() or SpawnTestNode().
struct TransportPair {
IpczDriverHandle ours;
IpczDriverHandle theirs;
};
TransportPair CreateTransports();
// Helper used to support multiprocess TestNode invocation.
int RunAsChild();
private:
// Sets the transport to use when connecting to a broker via ConnectBroker.
// Must only be called once.
void SetTransport(IpczDriverHandle transport);
// Spawns a new node using an appropriate configuration for the current
// driver. Returns a controller which can be used to interact with the node
// outside of ipcz (e.g. to wait on its termination). `factory` is a function
// which can produce an in-process instance of the TestNode; `test_node_name`
// is a string which can be used to run the same TestNode subclass in a child
// process.
//
// If `portals_or_transport` is a span of IpczHandles, this creates a new
// pair of transports. One is given to the new node for connection back to us,
// and the other is connected immediately by the broker, filling in the
// handles with initial portals for the connection.
//
// Otherwise it's assumed to be a transport that will be given to the new
// node for connecting back to us. In this case the caller is responsible for
// the transport's peer.
using PortalsOrTransport =
absl::variant<absl::Span<IpczHandle>, IpczDriverHandle>;
Ref<TestNodeController> SpawnTestNodeImpl(
IpczHandle from_node,
const internal::TestNodeDetails& details,
PortalsOrTransport portals_or_transport);
DriverMode driver_mode_ = DriverMode::kSync;
IpczHandle node_ = IPCZ_INVALID_HANDLE;
IpczDriverHandle transport_ = IPCZ_INVALID_DRIVER_HANDLE;
std::vector<Ref<TestNodeController>> spawned_nodes_;
#if BUILDFLAG(ENABLE_IPCZ_MULTIPROCESS_TESTS)
TestChildLauncher child_launcher_;
#endif
};
// Actual parameterized GTest Test fixture for multinode tests. This or a
// subclass of it is required for TEST_P() invocations to function as proper
// multinode tests.
template <typename TestNodeType = TestNode>
class MultinodeTest : public TestNodeType,
public ::testing::Test,
public ::testing::WithParamInterface<DriverMode> {
public:
static_assert(internal::IsValidTestNodeType<TestNodeType>,
"MultinodeTest<T> requires T to be a subclass of TestNode.");
MultinodeTest() {
TestNode::Initialize(GetParam(), IPCZ_CREATE_NODE_AS_BROKER);
}
};
} // namespace ipcz::test
#define MULTINODE_TEST_CHILD_MAIN_HELPER(func, node_name) \
MULTIPROCESS_TEST_MAIN(func) { \
node_name node; \
return node.RunAsChild(); \
}
#define MULTINODE_TEST_CHILD_MAIN(fixture, node_name) \
MULTINODE_TEST_CHILD_MAIN_HELPER(fixture##_##node_name##_Node, node_name)
// Defines the main body of a non-broker test node for a multinode test. The
// named node can be spawned by another node using SpawnTestNode<T> where T is
// the unique name given by `node_name` here. `fixture` must be
/// ipcz::test::TestNode or a subclass thereof.
#define MULTINODE_TEST_NODE(fixture, node_name) \
class node_name : public fixture { \
static_assert(::ipcz::test::internal::IsValidTestNodeType<fixture>, \
"MULTINODE_TEST_NODE() requires a fixture derived from " \
"ipcz::test::TestNode."); \
\
public: \
static constexpr ::ipcz::test::internal::TestNodeDetails kDetails = { \
.name = #fixture "_" #node_name "_Node", \
.factory = &::ipcz::test::internal::MakeTestNode<node_name>, \
}; \
void NodeBody() override; \
}; \
MULTINODE_TEST_CHILD_MAIN(fixture, node_name); \
void node_name::NodeBody()
#if BUILDFLAG(ENABLE_IPCZ_MULTIPROCESS_TESTS)
#define IPCZ_EXTRA_DRIVER_MODES , ipcz::test::DriverMode::kMultiprocess
#else
#define IPCZ_EXTRA_DRIVER_MODES
#endif
// TODO: Add other DriverMode enumerators here as support is landed.
#define INSTANTIATE_MULTINODE_TEST_SUITE_P(suite) \
INSTANTIATE_TEST_SUITE_P( \
, suite, \
::testing::Values(ipcz::test::DriverMode::kSync, \
ipcz::test::DriverMode::kAsync, \
ipcz::test::DriverMode::kAsyncDelegatedAlloc \
IPCZ_EXTRA_DRIVER_MODES))
#endif // IPCZ_SRC_TEST_MULTINODE_TEST_H_