blob: eb0fae364d4df70372f0ec01fdc6e8d7d7fbafc4 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// 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 <utility>
#include <vector>
#include "ipcz/ipcz.h"
#include "test/test_base.h"
#include "test_buildflags.h"
#include "testing/gtest/include/gtest/gtest.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 "util/ref_counted.h"
namespace ipcz::test {
class TestNode;
template <typename TestNodeType>
class MultinodeTest;
namespace internal {
template <typename TestNodeType>
std::unique_ptr<TestNode> MakeTestNode() {
return std::make_unique<TestNodeType>();
}
template <typename T>
static constexpr bool IsValidTestNodeType = std::is_base_of_v<TestNode, T>;
extern const char kSyncTestDriverName[];
extern const char kAsyncTestDriverName[];
extern const char kAsyncDelegatedAllocTestDriverName[];
extern const char kAsyncForcedBrokeringTestDriverName[];
extern const char kAsyncDelegatedAllocAndForcedBrokeringTestDriverName[];
extern const char kMultiprocessTestDriverName[];
} // namespace internal
class TestDriver;
using TestNodeFactory = std::unique_ptr<TestNode> (*)();
// Type used to package metadata about a MULTINODE_TEST_NODE() or
// MULTINODE_TEST() invocation.
struct TestNodeDetails {
// A unique display name for the defined node body.
const std::string_view name;
// A factory function which can be used to instantiate the TestNode. Null for
// main MULTINODE_TEST() invocations.
const TestNodeFactory factory;
// Indicates whether the node is defined as a broker or non-broker node. By
// default, all nodes on non-brokers except for those emitted by main
// MULTINODE_TEST() invocations.
const bool is_broker;
};
// 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. MULTINODE_TEST() 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_; }
// Returns metadata regarding the definition of this TestNode type.
virtual const TestNodeDetails& GetDetails() const = 0;
// 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 active TestDriver implementation.
TestDriver* GetTestDriver() { return test_driver_; }
// The ipcz driver currently in use, as specified by the active TestDriver.
const IpczDriver& GetDriver() const;
// One-time initialization. Called internally during test setup. Should never
// be called by individual test code.
void Initialize(TestDriver* test_driver);
// May be called at most once by the TestNode body to connect initial
// `portals` to the node that spawned this one. Extra `flags` may be passed to
// the corresponding ConnectNode() call.
void ConnectToParent(absl::Span<IpczHandle> portals,
IpczConnectNodeFlags flags = IPCZ_NO_FLAGS);
// May be called instead of ConnectToParent() when the portal that spawned
// this one is a broker.
void ConnectToBroker(absl::Span<IpczHandle> portals);
// Shorthand for the above, for the common case with only one initial portal.
IpczHandle ConnectToParent(IpczConnectNodeFlags flags = IPCZ_NO_FLAGS);
IpczHandle ConnectToBroker();
// Opens a new portal pair on this node.
std::pair<IpczHandle, IpczHandle> OpenPortals();
// Creates a new driver memory object populated with `contents`, boxes it, and
// returns a handle to the new box.
IpczHandle BoxBlob(std::string_view contents);
// Extracts the string contents of a boxed driver memory object.
std::string UnboxBlob(IpczHandle box);
// 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,
IpczConnectNodeFlags flags = IPCZ_NO_FLAGS) {
IpczDriverHandle our_transport;
auto controller = SpawnTestNodeImpl(TestNodeType::kDetails, our_transport);
if (TestNodeType::kDetails.is_broker) {
flags |= IPCZ_CONNECT_NODE_TO_BROKER;
}
const IpczResult result = ipcz().ConnectNode(
node(), our_transport, portals.size(), flags, nullptr, portals.data());
ABSL_ASSERT(result == IPCZ_RESULT_OK);
return controller;
}
// 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(IpczConnectNodeFlags flags = IPCZ_NO_FLAGS) {
IpczHandle portal;
SpawnTestNode<TestNodeType>({&portal, 1}, flags);
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> SpawnTestNodeNoConnect(IpczDriverHandle& transport) {
return SpawnTestNodeImpl(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 MULTINODE_TEST() 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();
// Creates a pair of transport appropriate for connecting two broker nodes
// together.
TransportPair CreateBrokerToBrokerTransports();
// Helper used to support multiprocess TestNode invocation.
int RunAsChild(TestDriver* test_driver);
// Sets the transport to use when connecting to a broker via ConnectBroker.
// Must only be called once.
void SetTransport(IpczDriverHandle transport);
private:
// 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). `details` describes the
// node to be launched, and `our_transport` on output receives a locally owned
// transport to the spawned node.
Ref<TestNodeController> SpawnTestNodeImpl(const TestNodeDetails& details,
IpczDriverHandle& our_transport);
TestDriver* test_driver_ = nullptr;
IpczHandle node_ = IPCZ_INVALID_HANDLE;
IpczDriverHandle transport_ = IPCZ_INVALID_DRIVER_HANDLE;
std::vector<Ref<TestNodeController>> spawned_nodes_;
};
// Actual parameterized GTest Test fixture for multinode tests. This or a
// subclass of it is required for MULTINODE_TEST() invocations to function as
// proper multinode tests.
template <typename TestNodeType = TestNode>
class MultinodeTest : public TestNodeType, public ::testing::Test {
public:
static_assert(internal::IsValidTestNodeType<TestNodeType>,
"MultinodeTest<T> requires T to be a subclass of TestNode.");
};
// TestDriver specifies an IpczDriver implementation to use for multinode tests.
// It also implements launching and joining of other test nodes. A TestDriver
// can be registered by statically initializing a corresponding
// TestDriverRegistration. All multinode tests are run against all registered
// TestDrivers.
class TestDriver {
public:
// A reference to the actual IpczDriver implementation used by this
// TestDriver.
virtual const IpczDriver& GetIpczDriver() const = 0;
// A unique name for this test driver.
virtual const char* GetName() const = 0;
// Creates a new pair of transports suitable for connecting a broker node to
// another node. Called by `source`, who will adopt `ours` from the returned
// pair and pass `theirs` to some newly spawned test node. If
// `for_broker_target` is true, `theirs` will be suitable for passing to a
// broker node; otherwise it must be passed to a non-broker.
virtual TestNode::TransportPair CreateTransports(
TestNode& source,
bool for_broker_target) const = 0;
// Spawns a new TestNode instance for a TestNode described by `details`,
// passing `their_transport` to the new node so it can establish a connection.
// `our_transport` is the local driver transport endpoint that will be used to
// connect to the new node.
virtual Ref<TestNode::TestNodeController> SpawnTestNode(
TestNode& source,
const TestNodeDetails& details,
IpczDriverHandle our_transport,
IpczDriverHandle their_transport) = 0;
// Returns any extra flags to be provided to ConnectNode() when connecting to
// the main test node from a node spawned by the test.
virtual IpczConnectNodeFlags GetExtraClientConnectNodeFlags() const = 0;
// If the test driver launches test nodes in a separate subprocess, this is
// called to retrieve the driver transport which the test node should use to
// connect to the broker.
virtual IpczDriverHandle GetClientTestNodeTransport() = 0;
};
// Registers a TestDriver globally so that all MULTINODE_TEST() invocations are
// parameterized over it.
class TestDriverRegistrationImpl {
public:
explicit TestDriverRegistrationImpl(TestDriver& driver);
};
template <typename TestDriverType>
class TestDriverRegistration {
public:
template <typename... Args>
explicit TestDriverRegistration(Args&&... args)
: driver(std::forward<Args>(args)...) {}
TestDriverType driver;
TestDriverRegistrationImpl registration{driver};
};
void RegisterMultinodeTestNode(std::string_view node_name,
TestNodeFactory factory);
template <typename NodeType>
class MultinodeTestNodeRegistration {
public:
MultinodeTestNodeRegistration() {
RegisterMultinodeTestNode(NodeType::kDetails.name,
NodeType::kDetails.factory);
}
};
using MultinodeTestFactory = std::function<testing::Test*(TestDriver*)>;
void RegisterMultinodeTest(const char* test_suite_name,
const char* test_name,
const char* filename,
int line,
MultinodeTestFactory factory);
// Registers a MULTINODE_TEST() test to be run when all tests are run. This
// registers a unique instance of the test for each registered test driver.
template <typename Test>
class MultinodeTestRegistration {
public:
MultinodeTestRegistration(const char* test_suite_name,
const char* test_name,
const char* filename,
int line) {
RegisterMultinodeTest(test_suite_name, test_name, filename, line,
[](TestDriver* driver) { return new Test(driver); });
}
};
// Must be called before RUN_ALL_TESTS() is invoked in order for any defined
// multinode tests to be run.
void RegisterMultinodeTests();
} // namespace ipcz::test
// 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_IMPL(fixture, node_name, is_broker_value) \
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::TestNodeDetails kDetails = { \
.name = #fixture "_" #node_name "_Node", \
.factory = &::ipcz::test::internal::MakeTestNode<node_name>, \
.is_broker = is_broker_value, \
}; \
const ::ipcz::test::TestNodeDetails& GetDetails() const override { \
return kDetails; \
} \
void NodeBody() override; \
}; \
::ipcz::test::MultinodeTestNodeRegistration<node_name> \
kRegister_##node_name; \
void node_name::NodeBody()
#define MULTINODE_TEST_NODE(fixture, node_name) \
MULTINODE_TEST_NODE_IMPL(fixture, node_name, /*is_broker=*/false)
#define MULTINODE_TEST_BROKER_NODE(fixture, node_name) \
MULTINODE_TEST_NODE_IMPL(fixture, node_name, /*is_broker=*/true)
#define MULTINODE_TEST_NAME(name) #name
#define MULTINODE_TEST_CLASS_NAME(name) name##_Test
#define MULTINODE_TEST_REGISTRATION_NAME(name) kRegister_##name##_Test
#define MULTINODE_TEST(fixture, test_name) \
class MULTINODE_TEST_CLASS_NAME(test_name) : public fixture { \
public: \
static constexpr ::ipcz::test::TestNodeDetails kDetails = { \
.name = #fixture "_" #test_name "_Node", \
.factory = nullptr, \
.is_broker = true, \
}; \
explicit MULTINODE_TEST_CLASS_NAME(test_name)(::ipcz::test::TestDriver * \
test_driver) { \
TestNode::Initialize(test_driver); \
} \
~MULTINODE_TEST_CLASS_NAME(test_name)() override = default; \
MULTINODE_TEST_CLASS_NAME(test_name) \
(const MULTINODE_TEST_CLASS_NAME(test_name) &) = delete; \
void operator=(const MULTINODE_TEST_CLASS_NAME(test_name) &) = delete; \
const ::ipcz::test::TestNodeDetails& GetDetails() const override { \
return kDetails; \
} \
\
private: \
void TestBody() override; \
}; \
namespace { \
::ipcz::test::MultinodeTestRegistration< \
MULTINODE_TEST_CLASS_NAME(test_name)> \
MULTINODE_TEST_REGISTRATION_NAME(test_name){ \
#fixture, MULTINODE_TEST_NAME(test_name), __FILE__, __LINE__}; \
} \
void MULTINODE_TEST_CLASS_NAME(test_name)::TestBody()
#endif // IPCZ_SRC_TEST_MULTINODE_TEST_H_