| // 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_ |