ipcz: Refactor multinode tests
Changes how multinode tests are expressed.
Introduces a new TestNode class which creates and owns a single
configurable node and exposes facilities for launching and
interconnecting new nodes.
A new MULTINODE_TEST_NODE(fixture, name) macro can be used to
conveniently subclass TestNode with a particular body of logic,
and such subclasses can be launched as new nodes by other
TestNodes.
Separately, the MultinodeTest fixture must now be templated over
a TestNode subclass (which the MultinodeTest itself also inherits,
in addition to GTest interfaces), and TEST_P() invocations which use
it are now implicitly run as a broker node.
This framework allows these integration tests to be run in any
test configuration, including a (not yet implemented) mode where
each spawned node runs in an isolated process.
Bug: 1299283
Change-Id: Idd02c4c08b1911497ea0ef3998e92dd5580f459e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3665020
Reviewed-by: Alex Gough <ajgo@chromium.org>
Commit-Queue: Ken Rockot <rockot@google.com>
Cr-Commit-Position: refs/heads/main@{#1007942}
NOKEYCHECK=True
GitOrigin-RevId: 0d9e10fd2efb69a21203036db095ca6ab4997b10
diff --git a/src/box_test.cc b/src/box_test.cc
index 57ff373..32851e1 100644
--- a/src/box_test.cc
+++ b/src/box_test.cc
@@ -16,7 +16,8 @@
namespace ipcz {
namespace {
-using BoxTest = test::MultinodeTestWithDriver;
+using BoxTestNode = test::TestNode;
+using BoxTest = test::MultinodeTest<BoxTestNode>;
using Blob = reference_drivers::Blob;
@@ -54,15 +55,13 @@
}
TEST_P(BoxTest, BoxAndUnbox) {
- IpczHandle node = CreateBrokerNode();
-
constexpr const char kMessage[] = "Hello, world?";
IpczDriverHandle blob_handle =
Blob::ReleaseAsHandle(MakeRefCounted<Blob>(kMessage));
IpczHandle box;
EXPECT_EQ(IPCZ_RESULT_OK,
- ipcz().Box(node, blob_handle, IPCZ_NO_FLAGS, nullptr, &box));
+ ipcz().Box(node(), blob_handle, IPCZ_NO_FLAGS, nullptr, &box));
blob_handle = IPCZ_INVALID_DRIVER_HANDLE;
EXPECT_EQ(IPCZ_RESULT_OK,
@@ -70,37 +69,29 @@
Ref<Blob> blob = Blob::TakeFromHandle(blob_handle);
EXPECT_EQ(kMessage, blob->message());
-
- Close(node);
}
TEST_P(BoxTest, CloseBox) {
- IpczHandle node = CreateBrokerNode();
-
Ref<Blob> blob = MakeRefCounted<Blob>("meh");
Ref<Blob::RefCountedFlag> destroyed = blob->destruction_flag_for_testing();
IpczDriverHandle blob_handle = Blob::ReleaseAsHandle(std::move(blob));
IpczHandle box;
EXPECT_EQ(IPCZ_RESULT_OK,
- ipcz().Box(node, blob_handle, IPCZ_NO_FLAGS, nullptr, &box));
+ ipcz().Box(node(), blob_handle, IPCZ_NO_FLAGS, nullptr, &box));
EXPECT_FALSE(destroyed->get());
EXPECT_EQ(IPCZ_RESULT_OK, ipcz().Close(box, IPCZ_NO_FLAGS, nullptr));
EXPECT_TRUE(destroyed->get());
-
- Close(node);
}
TEST_P(BoxTest, Peek) {
- IpczHandle node = CreateBrokerNode();
-
constexpr const char kMessage[] = "Hello, world?";
IpczDriverHandle blob_handle =
Blob::ReleaseAsHandle(MakeRefCounted<Blob>(kMessage));
IpczHandle box;
EXPECT_EQ(IPCZ_RESULT_OK,
- ipcz().Box(node, blob_handle, IPCZ_NO_FLAGS, nullptr, &box));
+ ipcz().Box(node(), blob_handle, IPCZ_NO_FLAGS, nullptr, &box));
blob_handle = IPCZ_INVALID_DRIVER_HANDLE;
EXPECT_EQ(IPCZ_RESULT_OK,
@@ -118,84 +109,105 @@
Ref<Blob> released_blob = Blob::TakeFromHandle(blob_handle);
EXPECT_EQ(blob, released_blob.get());
-
- Close(node);
}
-TEST_P(BoxTest, TransferBox) {
- IpczHandle node0 = CreateBrokerNode();
- IpczHandle node1 = CreateNonBrokerNode();
- auto [a, b] = ConnectBrokerToNonBroker(node0, node1);
+constexpr const char kMessage1[] = "Hello, world?";
+constexpr const char kMessage2[] = "Hello, world!";
+constexpr const char kMessage3[] = "Hello! World!";
- constexpr const char kMessage1[] = "Hello, world?";
- constexpr const char kMessage2[] = "Hello, world!";
- constexpr const char kMessage3[] = "Hello! World!";
-
- IpczDriverHandle blob_handle = CreateTestBlob(kMessage1, kMessage2);
- IpczHandle box;
- EXPECT_EQ(IPCZ_RESULT_OK,
- ipcz().Box(node0, blob_handle, IPCZ_NO_FLAGS, nullptr, &box));
-
- Put(a, kMessage3, {&box, 1});
+MULTINODE_TEST_NODE(BoxTestNode, TransferBoxClient) {
+ IpczHandle b = ConnectToBroker();
std::string message;
+ IpczHandle box;
EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, &message, {&box, 1}));
EXPECT_EQ(kMessage3, message);
+ IpczDriverHandle blob_handle;
EXPECT_EQ(IPCZ_RESULT_OK,
ipcz().Unbox(box, IPCZ_NO_FLAGS, nullptr, &blob_handle));
EXPECT_TRUE(BlobContentsMatch(blob_handle, kMessage1, kMessage2));
- CloseAll({a, b, node1, node0});
+ Close(b);
}
-TEST_P(BoxTest, TransferBoxBetweenNonBrokers) {
- IpczHandle node0 = CreateBrokerNode();
- IpczHandle node1 = CreateNonBrokerNode();
- IpczHandle node2 = CreateNonBrokerNode();
+TEST_P(BoxTest, TransferBox) {
+ IpczHandle c = SpawnTestNode<TransferBoxClient>();
- auto [a, b] = ConnectBrokerToNonBroker(node0, node1);
- auto [c, d] = ConnectBrokerToNonBroker(node0, node2);
+ IpczDriverHandle blob_handle = CreateTestBlob(kMessage1, kMessage2);
+ IpczHandle box;
+ EXPECT_EQ(IPCZ_RESULT_OK,
+ ipcz().Box(node(), blob_handle, IPCZ_NO_FLAGS, nullptr, &box));
- // Create a new portal pair and send each end to one of the two non-brokers so
- // they'll establish a direct link.
- auto [e, f] = OpenPortals(node0);
- Put(a, "", {&e, 1});
- Put(c, "", {&f, 1});
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(c, kMessage3, {&box, 1}));
- std::string message;
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, &message, {&e, 1}));
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(d, &message, {&f, 1}));
+ Close(c);
+}
- constexpr const char kMessage1[] = "Hello, world?";
- constexpr const char kMessage2[] = "Hello, world!";
- constexpr const char kMessage3[] = "Hello! World!";
+constexpr size_t TransferBoxBetweenNonBrokersNumIterations = 50;
- // Send messages end-to-end in each direction from one non-broker to the
- // other, each carrying a box with some data and a driver object. This covers
- // message relaying for multinode tests running with forced object brokering
- // enabled.
- constexpr size_t kNumIterations = 10;
- for (size_t i = 0; i < kNumIterations; ++i) {
+MULTINODE_TEST_NODE(BoxTestNode, TransferBoxBetweenNonBrokersClient1) {
+ IpczHandle q;
+ IpczHandle b = ConnectToBroker();
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, nullptr, {&q, 1}));
+
+ for (size_t i = 0; i < TransferBoxBetweenNonBrokersNumIterations; ++i) {
IpczDriverHandle blob_handle = CreateTestBlob(kMessage1, kMessage2);
IpczHandle box;
EXPECT_EQ(IPCZ_RESULT_OK,
- ipcz().Box(node0, blob_handle, IPCZ_NO_FLAGS, nullptr, &box));
+ ipcz().Box(node(), blob_handle, IPCZ_NO_FLAGS, nullptr, &box));
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(q, kMessage3, {&box, 1}));
+ box = IPCZ_INVALID_DRIVER_HANDLE;
- const IpczHandle sender = i % 2 ? e : f;
- const IpczHandle receiver = i % 2 ? f : e;
+ std::string message;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(q, &message, {&box, 1}));
+ EXPECT_EQ(kMessage1, message);
+ EXPECT_EQ(IPCZ_RESULT_OK,
+ ipcz().Unbox(box, IPCZ_NO_FLAGS, nullptr, &blob_handle));
+ EXPECT_TRUE(BlobContentsMatch(blob_handle, kMessage2, kMessage3));
+ }
- Put(sender, kMessage3, {&box, 1});
+ CloseAll({q, b});
+}
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(receiver, &message, {&box, 1}));
+MULTINODE_TEST_NODE(BoxTestNode, TransferBoxBetweenNonBrokersClient2) {
+ IpczHandle p;
+ IpczHandle b = ConnectToBroker();
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, nullptr, {&p, 1}));
+
+ for (size_t i = 0; i < TransferBoxBetweenNonBrokersNumIterations; ++i) {
+ IpczHandle box;
+ IpczDriverHandle blob_handle;
+ std::string message;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(p, &message, {&box, 1}));
EXPECT_EQ(kMessage3, message);
-
EXPECT_EQ(IPCZ_RESULT_OK,
ipcz().Unbox(box, IPCZ_NO_FLAGS, nullptr, &blob_handle));
EXPECT_TRUE(BlobContentsMatch(blob_handle, kMessage1, kMessage2));
+
+ blob_handle = CreateTestBlob(kMessage2, kMessage3);
+ EXPECT_EQ(IPCZ_RESULT_OK,
+ ipcz().Box(node(), blob_handle, IPCZ_NO_FLAGS, nullptr, &box));
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(p, kMessage1, {&box, 1}));
}
- CloseAll({a, b, c, d, e, f, node2, node1, node0});
+ CloseAll({p, b});
+}
+
+TEST_P(BoxTest, TransferBoxBetweenNonBrokers) {
+ IpczHandle c1 = SpawnTestNode<TransferBoxBetweenNonBrokersClient1>();
+ IpczHandle c2 = SpawnTestNode<TransferBoxBetweenNonBrokersClient2>();
+
+ // Create a new portal pair and send each end to one of the two non-brokers so
+ // they'll establish a direct link.
+ auto [q, p] = OpenPortals();
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(c1, "", {&q, 1}));
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(c2, "", {&p, 1}));
+
+ // Wait for the clients to finish their business and go away.
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(c1, IPCZ_TRAP_PEER_CLOSED));
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(c2, IPCZ_TRAP_PEER_CLOSED));
+ CloseAll({c1, c2});
}
INSTANTIATE_MULTINODE_TEST_SUITE_P(BoxTest);
diff --git a/src/connect_test.cc b/src/connect_test.cc
index 04334d9..1a717db 100644
--- a/src/connect_test.cc
+++ b/src/connect_test.cc
@@ -9,173 +9,121 @@
#include "test/multinode_test.h"
#include "test/test_transport_listener.h"
#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/synchronization/notification.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace ipcz {
namespace {
-using ConnectTest = test::MultinodeTestWithDriver;
+using ConnectTestNode = test::TestNode;
+using ConnectTest = test::MultinodeTest<ConnectTestNode>;
+
+MULTINODE_TEST_NODE(ConnectTestNode, BrokerToNonBrokerClient) {
+ IpczHandle b = ConnectToBroker();
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(b, IPCZ_TRAP_PEER_CLOSED));
+ Close(b);
+}
TEST_P(ConnectTest, BrokerToNonBroker) {
- IpczHandle broker = CreateBrokerNode();
- IpczHandle non_broker = CreateNonBrokerNode();
+ IpczHandle c = SpawnTestNode<BrokerToNonBrokerClient>();
+ Close(c);
+}
- IpczDriverHandle broker_transport;
- IpczDriverHandle non_broker_transport;
- CreateBrokerToNonBrokerTransports(&broker_transport, &non_broker_transport);
+constexpr size_t kNumBrokerPortals = 2;
+constexpr size_t kNumNonBrokerPortals = 5;
+static_assert(kNumBrokerPortals < kNumNonBrokerPortals,
+ "Test requires fewer broker portals than non-broker portals");
- IpczHandle non_broker_portal;
- ASSERT_EQ(IPCZ_RESULT_OK, ipcz().ConnectNode(non_broker, non_broker_transport,
- 1, IPCZ_CONNECT_NODE_TO_BROKER,
- nullptr, &non_broker_portal));
+MULTINODE_TEST_NODE(ConnectTestNode, SurplusPortalsClient) {
+ IpczHandle portals[kNumNonBrokerPortals];
+ ConnectToBroker(portals);
- IpczHandle broker_portal;
- ASSERT_EQ(IPCZ_RESULT_OK,
- ipcz().ConnectNode(broker, broker_transport, 1, IPCZ_NO_FLAGS,
- nullptr, &broker_portal));
-
- Close(broker_portal);
- EXPECT_EQ(IPCZ_RESULT_OK,
- WaitForConditionFlags(non_broker_portal, IPCZ_TRAP_PEER_CLOSED));
-
- CloseAll({non_broker_portal, non_broker, broker});
+ // All of the surplus portals should observe peer closure.
+ for (size_t i = kNumBrokerPortals; i < kNumNonBrokerPortals; ++i) {
+ EXPECT_EQ(IPCZ_RESULT_OK,
+ WaitForConditionFlags(portals[i], IPCZ_TRAP_PEER_CLOSED));
+ }
+ CloseAll(portals);
}
TEST_P(ConnectTest, SurplusPortals) {
- IpczHandle broker = CreateBrokerNode();
- IpczHandle non_broker = CreateNonBrokerNode();
-
- IpczDriverHandle broker_transport;
- IpczDriverHandle non_broker_transport;
- CreateBrokerToNonBrokerTransports(&broker_transport, &non_broker_transport);
-
- constexpr size_t kNumBrokerPortals = 2;
- constexpr size_t kNumNonBrokerPortals = 5;
- static_assert(kNumBrokerPortals < kNumNonBrokerPortals,
- "Test requires fewer broker portals than non-broker portals");
-
- IpczHandle broker_portals[kNumBrokerPortals];
- ASSERT_EQ(
- IPCZ_RESULT_OK,
- ipcz().ConnectNode(broker, broker_transport, std::size(broker_portals),
- IPCZ_NO_FLAGS, nullptr, broker_portals));
-
- IpczHandle non_broker_portals[kNumNonBrokerPortals];
- ASSERT_EQ(IPCZ_RESULT_OK, ipcz().ConnectNode(non_broker, non_broker_transport,
- std::size(non_broker_portals),
- IPCZ_CONNECT_NODE_TO_BROKER,
- nullptr, non_broker_portals));
-
- // All of the surplus broker portals should observe peer closure.
- for (size_t i = kNumBrokerPortals; i < kNumNonBrokerPortals; ++i) {
- EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(non_broker_portals[i],
- IPCZ_TRAP_PEER_CLOSED));
- }
-
- for (IpczHandle portal : non_broker_portals) {
- Close(portal);
- }
- for (IpczHandle portal : broker_portals) {
- Close(portal);
- }
- CloseAll({non_broker, broker});
+ IpczHandle portals[kNumBrokerPortals];
+ SpawnTestNode<SurplusPortalsClient>(portals);
+ CloseAll(portals);
}
-TEST_P(ConnectTest, DisconnectWithoutHandshake) {
- IpczHandle broker = CreateBrokerNode();
- IpczHandle non_broker = CreateNonBrokerNode();
-
- // First fail to connect a broker.
- IpczDriverHandle broker_transport, non_broker_transport;
- CreateBrokerToNonBrokerTransports(&broker_transport, &non_broker_transport);
-
- IpczHandle portal;
- {
- // This listener is scoped such that it closes the non-broker's transport
- // after the broker issues its ConnectNode(). This should trigger a
- // rejection and ultimately portal closure by the broker.
- test::TestTransportListener non_broker_listener(non_broker,
- non_broker_transport);
- non_broker_listener.DiscardMessages<msg::ConnectFromBrokerToNonBroker>();
-
- ASSERT_EQ(IPCZ_RESULT_OK,
- ipcz().ConnectNode(broker, broker_transport, 1, IPCZ_NO_FLAGS,
- nullptr, &portal));
- }
-
- EXPECT_EQ(IPCZ_RESULT_OK,
- WaitForConditionFlags(portal, IPCZ_TRAP_PEER_CLOSED));
- Close(portal);
-
- // Next fail to connect a non-broker.
- CreateBrokerToNonBrokerTransports(&broker_transport, &non_broker_transport);
-
- {
- // This listener is scoped such that it closes the broker transport after
- // the non-broker issues its ConnectNode(). This should trigger a rejection
- // and ultimately portal closure by the non-broker.
- test::TestTransportListener broker_listener(broker, broker_transport);
- broker_listener.DiscardMessages<msg::ConnectFromNonBrokerToBroker>();
-
- ASSERT_EQ(
- IPCZ_RESULT_OK,
- ipcz().ConnectNode(non_broker, non_broker_transport, 1,
- IPCZ_CONNECT_NODE_TO_BROKER, nullptr, &portal));
- }
-
- EXPECT_EQ(IPCZ_RESULT_OK,
- WaitForConditionFlags(portal, IPCZ_TRAP_PEER_CLOSED));
-
- CloseAll({portal, non_broker, broker});
+MULTINODE_TEST_NODE(ConnectTestNode, ExpectDisconnectFromBroker) {
+ IpczHandle b = ConnectToBroker();
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(b, IPCZ_TRAP_PEER_CLOSED));
+ Close(b);
}
-TEST_P(ConnectTest, DisconnectOnBadMessage) {
- IpczHandle broker = CreateBrokerNode();
- IpczHandle non_broker = CreateNonBrokerNode();
+TEST_P(ConnectTest, DisconnectWithoutBrokerHandshake) {
+ TransportPair transports = CreateTransports();
+ auto controller =
+ SpawnTestNode<ExpectDisconnectFromBroker>(transports.theirs);
+ EXPECT_EQ(IPCZ_RESULT_OK,
+ GetDriver().Close(transports.ours, IPCZ_NO_FLAGS, nullptr));
+ controller->WaitForShutdown();
+}
- IpczDriverHandle broker_transport, non_broker_transport;
- CreateBrokerToNonBrokerTransports(&broker_transport, &non_broker_transport);
+MULTINODE_TEST_NODE(ConnectTestNode,
+ DisconnectWithoutNonBrokerHandshakeClient) {
+ // Our transport is automatically closed on exit. No handshake is sent because
+ // we never call ConnectToBroker(). No action required.
+}
- // First fail to connect a broker.
- IpczHandle portal;
- ASSERT_EQ(IPCZ_RESULT_OK,
- ipcz().ConnectNode(broker, broker_transport, 1, IPCZ_NO_FLAGS,
- nullptr, &portal));
+TEST_P(ConnectTest, DisconnectWithoutNonBrokerHandshake) {
+ IpczHandle c = SpawnTestNode<DisconnectWithoutNonBrokerHandshakeClient>();
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(c, IPCZ_TRAP_PEER_CLOSED));
+ Close(c);
+}
- test::TestTransportListener non_broker_listener(non_broker,
- non_broker_transport);
- non_broker_listener.DiscardMessages<msg::ConnectFromBrokerToNonBroker>();
+TEST_P(ConnectTest, DisconnectOnBadBrokerMessage) {
+ TransportPair transports = CreateTransports();
+ auto controller =
+ SpawnTestNode<ExpectDisconnectFromBroker>(transports.theirs);
+ // Send some garbage to the other node.
const char kBadMessage[] = "this will never be a valid handshake message!";
+ EXPECT_EQ(
+ IPCZ_RESULT_OK,
+ GetDriver().Transmit(transports.ours, kBadMessage, std::size(kBadMessage),
+ nullptr, 0, IPCZ_NO_FLAGS, nullptr));
EXPECT_EQ(IPCZ_RESULT_OK,
- GetDriver().Transmit(non_broker_transport, kBadMessage,
- std::size(kBadMessage), nullptr, 0,
- IPCZ_NO_FLAGS, nullptr));
+ GetDriver().Close(transports.ours, IPCZ_NO_FLAGS, nullptr));
- EXPECT_EQ(IPCZ_RESULT_OK,
- WaitForConditionFlags(portal, IPCZ_TRAP_PEER_CLOSED));
+ // The other node will only shut down once it's observed peer closure on its
+ // portal to us; which it should, because we just sent it some garbage.
+ controller->WaitForShutdown();
+}
- non_broker_listener.StopListening();
- Close(portal);
+MULTINODE_TEST_NODE(ConnectTestNode, TransmitSomeGarbage) {
+ // Instead of doing the usual connection dance, send some garbage back to the
+ // broker. It should disconnect ASAP.
+ const char kBadMessage[] = "this will never be a valid handshake message!";
+ EXPECT_EQ(
+ IPCZ_RESULT_OK,
+ GetDriver().Transmit(transport(), kBadMessage, std::size(kBadMessage),
+ nullptr, 0, IPCZ_NO_FLAGS, nullptr));
- // Next fail to connect a non-broker.
- CreateBrokerToNonBrokerTransports(&broker_transport, &non_broker_transport);
+ test::TestTransportListener listener(node(), ReleaseTransport());
+ absl::Notification done;
+ listener.OnError([&done] { done.Notify(); });
+ done.WaitForNotification();
+ listener.StopListening();
+}
- test::TestTransportListener broker_listener(broker, broker_transport);
- broker_listener.DiscardMessages<msg::ConnectFromNonBrokerToBroker>();
+TEST_P(ConnectTest, DisconnectOnBadNonBrokerMessage) {
+ IpczHandle c;
+ auto controller = SpawnTestNode<TransmitSomeGarbage>({&c, 1});
- ASSERT_EQ(IPCZ_RESULT_OK,
- ipcz().ConnectNode(non_broker, non_broker_transport, 1,
- IPCZ_CONNECT_NODE_TO_BROKER, nullptr, &portal));
- EXPECT_EQ(IPCZ_RESULT_OK,
- GetDriver().Transmit(broker_transport, kBadMessage,
- std::size(kBadMessage), nullptr, 0,
- IPCZ_NO_FLAGS, nullptr));
- EXPECT_EQ(IPCZ_RESULT_OK,
- WaitForConditionFlags(portal, IPCZ_TRAP_PEER_CLOSED));
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(c, IPCZ_TRAP_PEER_CLOSED));
+ Close(c);
- broker_listener.StopListening();
- CloseAll({portal, non_broker, broker});
+ // Make sure the client also observes disconnection of its transport. It won't
+ // terminate until that happens.
+ controller->WaitForShutdown();
}
INSTANTIATE_MULTINODE_TEST_SUITE_P(ConnectTest);
diff --git a/src/remote_portal_test.cc b/src/remote_portal_test.cc
index 45ecbdb..bbcef8d 100644
--- a/src/remote_portal_test.cc
+++ b/src/remote_portal_test.cc
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include <string>
+#include <string_view>
#include <utility>
#include "ipcz/ipcz.h"
@@ -11,144 +13,214 @@
namespace ipcz {
namespace {
-using RemotePortalTest = test::MultinodeTestWithDriver;
+using RemotePortalTestNode = test::TestNode;
+using RemotePortalTest = test::MultinodeTest<RemotePortalTestNode>;
+
+static constexpr std::string_view kTestMessage1 = "hello world";
+static constexpr std::string_view kTestMessage2 = "hola mundo";
+
+MULTINODE_TEST_NODE(RemotePortalTestNode, BasicConnectionClient) {
+ IpczHandle b = ConnectToBroker();
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(b, kTestMessage1));
+
+ std::string message;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, &message));
+ EXPECT_EQ(kTestMessage2, message);
+ Close(b);
+}
TEST_P(RemotePortalTest, BasicConnection) {
- IpczHandle broker = CreateBrokerNode();
- IpczHandle non_broker = CreateNonBrokerNode();
- auto [a, b] = ConnectBrokerToNonBroker(broker, non_broker);
+ IpczHandle c = SpawnTestNode<BasicConnectionClient>();
- VerifyEndToEnd(a, b);
+ std::string message;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(c, &message));
+ EXPECT_EQ(kTestMessage1, message);
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(c, kTestMessage2));
+ Close(c);
+}
- CloseAll({a, b, non_broker, broker});
+MULTINODE_TEST_NODE(RemotePortalTestNode, PortalTransferClient) {
+ IpczHandle b = ConnectToBroker();
+
+ IpczHandle p = IPCZ_INVALID_HANDLE;
+ std::string message;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, &message, {&p, 1}));
+ EXPECT_EQ(kTestMessage1, message);
+ EXPECT_NE(IPCZ_INVALID_HANDLE, p);
+
+ VerifyEndToEnd(p);
+ CloseAll({p, b});
}
TEST_P(RemotePortalTest, PortalTransfer) {
- IpczHandle broker = CreateBrokerNode();
- IpczHandle non_broker = CreateNonBrokerNode();
- auto [a, b] = ConnectBrokerToNonBroker(broker, non_broker);
- auto [c, d] = OpenPortals(broker);
+ IpczHandle c = SpawnTestNode<PortalTransferClient>();
- // Send portal `d` to the non-broker node.
- const std::string kMessage = "hello";
- EXPECT_EQ(IPCZ_RESULT_OK, Put(a, kMessage, {&d, 1}));
- d = IPCZ_INVALID_HANDLE;
+ auto [q, p] = OpenPortals();
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(c, kTestMessage1, {&p, 1}));
- // Retrieve portal `d` from the sent parcel.
- std::string message;
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, &message, {&d, 1}));
- EXPECT_EQ(kMessage, message);
- EXPECT_NE(IPCZ_INVALID_HANDLE, d);
+ VerifyEndToEnd(q);
+ CloseAll({q, c});
+}
- // Portals `c` and `d` should be able to communicate end-to-end across the
- // node boundary.
- VerifyEndToEnd(c, d);
+constexpr size_t kMultipleHopsNumIterations = 100;
- CloseAll({a, b, c, d, non_broker, broker});
+MULTINODE_TEST_NODE(RemotePortalTestNode, MultipleHopsClient1) {
+ IpczHandle b = ConnectToBroker();
+
+ auto [q, p] = OpenPortals();
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(b, "", {&p, 1}));
+
+ for (size_t i = 0; i < kMultipleHopsNumIterations; ++i) {
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(q, kTestMessage2));
+ }
+
+ for (size_t i = 0; i < kMultipleHopsNumIterations; ++i) {
+ std::string message;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(q, &message));
+ EXPECT_EQ(kTestMessage1, message);
+ }
+
+ CloseAll({q, b});
+}
+
+MULTINODE_TEST_NODE(RemotePortalTestNode, MultipleHopsClient2) {
+ IpczHandle b = ConnectToBroker();
+
+ IpczHandle p;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, nullptr, {&p, 1}));
+
+ for (size_t i = 0; i < kMultipleHopsNumIterations; ++i) {
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(p, kTestMessage1));
+ }
+
+ for (size_t i = 0; i < kMultipleHopsNumIterations; ++i) {
+ std::string message;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(p, &message));
+ EXPECT_EQ(kTestMessage2, message);
+ }
+
+ CloseAll({p, b});
}
TEST_P(RemotePortalTest, MultipleHops) {
- IpczHandle node0 = CreateBrokerNode();
- IpczHandle node1 = CreateNonBrokerNode();
- IpczHandle node2 = CreateNonBrokerNode();
+ IpczHandle c1 = SpawnTestNode<MultipleHopsClient1>();
+ IpczHandle c2 = SpawnTestNode<MultipleHopsClient2>();
- auto [a, b] = ConnectBrokerToNonBroker(node0, node1);
- auto [c, d] = ConnectBrokerToNonBroker(node0, node2);
- auto [e, f] = OpenPortals(node1);
+ IpczHandle p;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(c1, nullptr, {&p, 1}));
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(c2, "", {&p, 1}));
- // Send `f` from node1 to node0 and then from node0 to node2
- Put(b, "here", {&f, 1});
- f = IPCZ_INVALID_HANDLE;
+ CloseAll({c1, c2});
+}
- std::string message;
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(a, &message, {&f, 1}));
- ASSERT_NE(IPCZ_INVALID_HANDLE, f);
+constexpr size_t kTransferBackAndForthNumIterations = 1;
- Put(c, "ok ok", {&f, 1});
- f = IPCZ_INVALID_HANDLE;
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(d, &message, {&f, 1}));
- ASSERT_NE(IPCZ_INVALID_HANDLE, f);
+MULTINODE_TEST_NODE(RemotePortalTestNode, TransferBackAndForthClient) {
+ IpczHandle b = ConnectToBroker();
- constexpr size_t kNumIterations = 100;
- for (size_t i = 0; i < kNumIterations; ++i) {
- Put(e, "merp");
- Put(f, "nerp");
- }
- for (size_t i = 0; i < kNumIterations; ++i) {
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(f, &message));
- EXPECT_EQ("merp", message);
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(e, &message));
- EXPECT_EQ("nerp", message);
+ for (size_t i = 0; i < kTransferBackAndForthNumIterations; ++i) {
+ IpczHandle p;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, nullptr, {&p, 1}));
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(b, "", {&p, 1}));
}
- CloseAll({a, b, c, d, e, f, node2, node1, node0});
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(b, IPCZ_TRAP_PEER_CLOSED));
+ Close(b);
}
TEST_P(RemotePortalTest, TransferBackAndForth) {
- IpczHandle node0 = CreateBrokerNode();
- IpczHandle node1 = CreateNonBrokerNode();
+ IpczHandle c = SpawnTestNode<TransferBackAndForthClient>();
- auto [a, b] = ConnectBrokerToNonBroker(node0, node1);
- auto [c, d] = OpenPortals(node0);
-
+ constexpr std::string_view kMessage = "hihihihi";
+ auto [q, p] = OpenPortals();
std::string message;
- constexpr size_t kNumIterations = 8;
- for (size_t i = 0; i < kNumIterations; ++i) {
- Put(c, "hi");
- Put(a, "", {&d, 1});
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, &message, {&d, 1}));
- Put(b, "", {&d, 1});
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(a, &message, {&d, 1}));
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(d, &message));
- EXPECT_EQ("hi", message);
+ for (size_t i = 0; i < kTransferBackAndForthNumIterations; ++i) {
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(q, kMessage));
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(c, "", {&p, 1}));
+ p = IPCZ_INVALID_HANDLE;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(c, nullptr, {&p, 1}));
+ EXPECT_NE(IPCZ_INVALID_HANDLE, p);
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(p, &message));
+ EXPECT_EQ(kMessage, message);
}
- CloseAll({a, b});
- VerifyEndToEnd(c, d);
+ VerifyEndToEndLocal(q, p);
+ CloseAll({q, p, c});
+}
- CloseAll({c, d, node1, node0});
+MULTINODE_TEST_NODE(RemotePortalTestNode, DisconnectThroughProxyClient1) {
+ IpczHandle b = ConnectToBroker();
+
+ IpczHandle q;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, nullptr, {&q, 1}));
+
+ // Should eventually be observed by virtue of the forced disconnection of
+ // client 3 in the main test body.
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(q, IPCZ_TRAP_PEER_CLOSED));
+ CloseAll({q, b});
+}
+
+MULTINODE_TEST_NODE(RemotePortalTestNode, DisconnectThroughProxyClient2) {
+ IpczHandle b = ConnectToBroker();
+
+ // Receive a portal p from the broker and bounce it back.
+ IpczHandle p;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, nullptr, {&p, 1}));
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(b, "", {&p, 1}));
+
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(b, IPCZ_TRAP_PEER_CLOSED));
+ Close(b);
+}
+
+MULTINODE_TEST_NODE(RemotePortalTestNode, DisconnectThroughProxyClient3) {
+ IpczHandle b = ConnectToBroker();
+
+ // Receive a portal p from the broker and then immediately terminate this
+ // node.
+ IpczHandle p;
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, nullptr, {&p, 1}));
+
+ // Forcibly shut this node down, severing all connections to other nodes. We
+ // do this *before* closing `p` to ensure that we are't just exercising normal
+ // portal closure, which other tests already cover. From the perspective of
+ // other nodes, we are effectively simulating a crash of this node.
+ CloseThisNode();
+
+ // It's still necessary to explicitly close local portals after the node
+ // itself has been shut down. Otherwise they'd leak.
+ CloseAll({p, b});
}
TEST_P(RemotePortalTest, DisconnectThroughProxy) {
// Exercises node disconnection. Namely if portals on nodes 1 and 3 are
// connected via proxy on node 2, and node 3 disappears, node 1's portal
// should observe peer closure.
- IpczHandle node0 = CreateBrokerNode();
- IpczHandle node1 = CreateNonBrokerNode();
- IpczHandle node2 = CreateNonBrokerNode();
- IpczHandle node3 = CreateNonBrokerNode();
+ IpczHandle c1, c3;
+ auto c1_control = SpawnTestNode<DisconnectThroughProxyClient1>({&c1, 1});
+ IpczHandle c2 = SpawnTestNode<DisconnectThroughProxyClient2>();
+ auto c3_control = SpawnTestNode<DisconnectThroughProxyClient3>({&c3, 1});
- auto [a, b] = ConnectBrokerToNonBroker(node0, node1);
- auto [c, d] = ConnectBrokerToNonBroker(node0, node2);
- auto [e, f] = ConnectBrokerToNonBroker(node0, node3);
+ auto [q, p] = OpenPortals();
- auto [q, p] = OpenPortals(node0);
+ // We send q to client 1, and p to client 2.
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(c1, "", {&q, 1}));
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(c2, "", {&p, 1}));
- // Send `q` to `node1` and `p` to `node2`.
- Put(a, "", {&q, 1});
- Put(c, "", {&p, 1});
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, nullptr, {&q, 1}));
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(d, nullptr, {&p, 1}));
+ // Client 2 forwards p back to us, and we forward it now to client 3. This
+ // process ensures that client 2 will for some time serve as a proxy between
+ // client 1 and client 3.
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(c2, nullptr, {&p, 1}));
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(c3, "", {&p, 1}));
- // Now forward 'p' back to `node0` and then again to `node3`. This ensures
- // that node2 will proxy between node1 and node3 for at least a small window
- // of time.
- Put(d, "", {&p, 1});
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(c, nullptr, {&p, 1}));
- Put(e, "", {&p, 1});
- EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(f, nullptr, {&p, 1}));
+ // Client 3 will terminate on its own. Though not determinstic, this will
+ // race with proxy reduction such that client 2 may still be proxying for the
+ // q-p portal pair when client 3 (who owns p) goes away.
+ EXPECT_TRUE(c3_control->WaitForShutdown());
- // Forcibly close node3 such that all its connections are severed. Any portals
- // reliant on those connections should observe peer closure as a result. Note
- // that portal lifetime is independent of node lifetime, so affected portals
- // created by node3 still must be explicitly closed below.
- Close(node3);
-
- // Even q must observe peer closure, despite being potentially several hops
- // away from node3 where its peer p resided.
- EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(q, IPCZ_TRAP_PEER_CLOSED));
-
- CloseAll({a, b, c, d, e, f, q, p, node2, node1, node0});
+ // Client 1 waits on q observing peer closure before terminating. This will
+ // block until that happens.
+ EXPECT_TRUE(c1_control->WaitForShutdown());
+ CloseAll({c1, c2, c3});
}
INSTANTIATE_MULTINODE_TEST_SUITE_P(RemotePortalTest);
diff --git a/src/test/multinode_test.cc b/src/test/multinode_test.cc
index f348657..88eaf71 100644
--- a/src/test/multinode_test.cc
+++ b/src/test/multinode_test.cc
@@ -4,91 +4,187 @@
#include "test/multinode_test.h"
+#include <thread>
+
#include "ipcz/ipcz.h"
#include "reference_drivers/single_process_reference_driver.h"
#include "third_party/abseil-cpp/absl/base/macros.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
+#include "util/log.h"
namespace ipcz::test {
namespace {
-const IpczDriver* GetDriverImpl(MultinodeTest::DriverMode mode) {
- switch (mode) {
- case MultinodeTest::DriverMode::kSync:
- return &reference_drivers::kSingleProcessReferenceDriver;
+// Launches a new node on a dedicated thread within the same process. All
+// connections use the synchronous single-process driver.
+class InProcessTestNodeController : public TestNode::TestNodeController {
+ public:
+ InProcessTestNodeController(DriverMode driver_mode,
+ std::unique_ptr<TestNode> test_node)
+ : client_thread_(absl::in_place,
+ &RunTestNode,
+ driver_mode,
+ std::move(test_node)) {}
- default:
- // Other modes not yet supported.
- return nullptr;
+ ~InProcessTestNodeController() override { ABSL_ASSERT(!client_thread_); }
+
+ // TestNode::TestNodeController:
+ bool WaitForShutdown() override {
+ if (client_thread_) {
+ client_thread_->join();
+ client_thread_.reset();
+ }
+
+ // In spirit, the point of WaitForShutdown()'s return value is to signal to
+ // the running test whether something went wrong in a spawned node. This is
+ // necessary to propagate test expectation failures from within child
+ // processes when running in a multiprocess test mode.
+ //
+ // When spawned nodes are running in the main test process however, their
+ // test expectation failures already affect the pass/fail state of the
+ // running test. In this case there's no need to propagate a redundant
+ // failure signal here, hence we always return true.
+ return true;
}
-}
-void DoConnect(const IpczAPI& ipcz,
- IpczHandle node,
- IpczDriverHandle transport,
- IpczConnectNodeFlags flags,
- IpczHandle& portal) {
- const IpczResult result =
- ipcz.ConnectNode(node, transport, 1, flags, nullptr, &portal);
- ASSERT_EQ(IPCZ_RESULT_OK, result);
-}
+ private:
+ static void RunTestNode(DriverMode driver_mode,
+ std::unique_ptr<TestNode> test_node) {
+ test_node->Initialize(driver_mode, IPCZ_NO_FLAGS);
+ test_node->NodeBody();
+ }
+
+ absl::optional<std::thread> client_thread_;
+};
} // namespace
-MultinodeTest::MultinodeTest() = default;
+TestNode::~TestNode() {
+ for (auto& spawned_node : spawned_nodes_) {
+ EXPECT_TRUE(spawned_node->WaitForShutdown());
+ }
-MultinodeTest::~MultinodeTest() = default;
+ // If we never connected to the broker, make sure we don't leak our transport.
+ if (transport_ != IPCZ_INVALID_DRIVER_HANDLE) {
+ GetDriver().Close(transport_, IPCZ_NO_FLAGS, nullptr);
+ }
-const IpczDriver& MultinodeTest::GetDriver(DriverMode mode) const {
- const IpczDriver* driver = GetDriverImpl(mode);
- ABSL_ASSERT(driver);
- return *driver;
+ CloseThisNode();
}
-IpczHandle MultinodeTest::CreateBrokerNode(DriverMode mode) {
- IpczHandle node;
- ipcz().CreateNode(&GetDriver(mode), IPCZ_INVALID_DRIVER_HANDLE,
- IPCZ_CREATE_NODE_AS_BROKER, nullptr, &node);
- return node;
+const IpczDriver& TestNode::GetDriver() const {
+ static IpczDriver kInvalidDriver = {};
+ switch (driver_mode_) {
+ case DriverMode::kSync:
+ return reference_drivers::kSingleProcessReferenceDriver;
+
+ default:
+ // Other modes not yet supported.
+ ABSL_ASSERT(false);
+ return kInvalidDriver;
+ }
}
-IpczHandle MultinodeTest::CreateNonBrokerNode(DriverMode mode) {
- IpczHandle node;
- ipcz().CreateNode(&GetDriver(mode), IPCZ_INVALID_DRIVER_HANDLE, IPCZ_NO_FLAGS,
- nullptr, &node);
- return node;
-}
+void TestNode::Initialize(DriverMode driver_mode,
+ IpczCreateNodeFlags create_node_flags) {
+ driver_mode_ = driver_mode;
-void MultinodeTest::CreateBrokerToNonBrokerTransports(
- DriverMode mode,
- IpczDriverHandle* transport0,
- IpczDriverHandle* transport1) {
- // TODO: Support other DriverModes.
- ABSL_ASSERT(mode == DriverMode::kSync);
- IpczResult result = GetDriver(mode).CreateTransports(
- IPCZ_INVALID_DRIVER_HANDLE, IPCZ_INVALID_DRIVER_HANDLE, IPCZ_NO_FLAGS,
- nullptr, transport0, transport1);
+ ABSL_ASSERT(node_ == IPCZ_INVALID_HANDLE);
+ const IpczResult result =
+ ipcz().CreateNode(&GetDriver(), IPCZ_INVALID_DRIVER_HANDLE,
+ create_node_flags, nullptr, &node_);
ABSL_ASSERT(result == IPCZ_RESULT_OK);
}
-std::pair<IpczHandle, IpczHandle> MultinodeTest::ConnectBrokerToNonBroker(
- DriverMode mode,
- IpczHandle broker_node,
- IpczHandle non_broker_node) {
- IpczDriverHandle broker_transport;
- IpczDriverHandle non_broker_transport;
- CreateBrokerToNonBrokerTransports(mode, &broker_transport,
- &non_broker_transport);
+void TestNode::ConnectToBroker(absl::Span<IpczHandle> portals) {
+ IpczDriverHandle transport =
+ std::exchange(transport_, IPCZ_INVALID_DRIVER_HANDLE);
+ ABSL_ASSERT(transport != IPCZ_INVALID_DRIVER_HANDLE);
+ const IpczResult result =
+ ipcz().ConnectNode(node(), transport, portals.size(),
+ IPCZ_CONNECT_NODE_TO_BROKER, nullptr, portals.data());
+ ASSERT_EQ(IPCZ_RESULT_OK, result);
+}
- IpczHandle broker_portal;
- DoConnect(ipcz(), broker_node, broker_transport, IPCZ_NO_FLAGS,
- broker_portal);
+IpczHandle TestNode::ConnectToBroker() {
+ IpczHandle portal;
+ ConnectToBroker({&portal, 1});
+ return portal;
+}
- IpczHandle non_broker_portal;
- DoConnect(ipcz(), non_broker_node, non_broker_transport,
- IPCZ_CONNECT_NODE_TO_BROKER, non_broker_portal);
+std::pair<IpczHandle, IpczHandle> TestNode::OpenPortals() {
+ return TestBase::OpenPortals(node_);
+}
- return {broker_portal, non_broker_portal};
+void TestNode::CloseThisNode() {
+ if (node_ != IPCZ_INVALID_HANDLE) {
+ IpczHandle node = std::exchange(node_, IPCZ_INVALID_HANDLE);
+ ipcz().Close(node, IPCZ_NO_FLAGS, nullptr);
+ }
+}
+
+Ref<TestNode::TestNodeController> TestNode::SpawnTestNodeImpl(
+ IpczHandle from_node,
+ const internal::TestNodeDetails& details,
+ PortalsOrTransport portals_or_transport) {
+ struct Connect {
+ explicit Connect(TestNode& test) : test(test) {}
+
+ IpczDriverHandle operator()(absl::Span<IpczHandle> portals) {
+ TransportPair transports = test.CreateTransports();
+ const IpczResult result =
+ test.ipcz().ConnectNode(test.node(), transports.ours, portals.size(),
+ IPCZ_NO_FLAGS, nullptr, portals.data());
+ ABSL_ASSERT(result == IPCZ_RESULT_OK);
+ return transports.theirs;
+ }
+
+ IpczDriverHandle operator()(IpczDriverHandle transport) {
+ return transport;
+ }
+
+ TestNode& test;
+ };
+
+ Connect connect(*this);
+ IpczDriverHandle their_transport = absl::visit(connect, portals_or_transport);
+
+ // TODO: Support a multiprocess mode which launches the new node in a child
+ // child process, passing the transport there.
+ std::unique_ptr<TestNode> test_node = details.factory();
+ test_node->SetTransport(their_transport);
+ Ref<TestNodeController> controller =
+ MakeRefCounted<InProcessTestNodeController>(driver_mode_,
+ std::move(test_node));
+ spawned_nodes_.push_back(controller);
+ return controller;
+}
+
+TestNode::TransportPair TestNode::CreateTransports() {
+ TransportPair transports;
+ switch (driver_mode_) {
+ case DriverMode::kSync: {
+ const IpczResult result =
+ reference_drivers::kSingleProcessReferenceDriver.CreateTransports(
+ IPCZ_INVALID_DRIVER_HANDLE, IPCZ_INVALID_DRIVER_HANDLE,
+ IPCZ_NO_FLAGS, nullptr, &transports.ours, &transports.theirs);
+ ABSL_ASSERT(result == IPCZ_RESULT_OK);
+ break;
+ }
+
+ default:
+ LOG(FATAL) << "DriverMode not yet supported.";
+ return {};
+ }
+
+ return transports;
+}
+
+void TestNode::SetTransport(IpczDriverHandle transport) {
+ ABSL_ASSERT(transport_ == IPCZ_INVALID_DRIVER_HANDLE);
+ transport_ = transport;
}
} // namespace ipcz::test
diff --git a/src/test/multinode_test.h b/src/test/multinode_test.h
index 5c60daf..99508f7 100644
--- a/src/test/multinode_test.h
+++ b/src/test/multinode_test.h
@@ -5,119 +5,260 @@
#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 "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 {
-// Base test fixture 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.
+class TestNode;
+
+template <typename TestNodeType>
+class MultinodeTest;
+
+// Selects which driver test nodes will use. Interconnecting nodes must always
+// use the same driver.
//
-// This fixture mostly provides convenience methods for creating and connecting
-// nodes in various useful configurations.
-class MultinodeTest : public internal::TestBase, public ::testing::Test {
- public:
- // Selects which driver a new node will use. 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 deterministic and generally
- // easier to debug in such cases.
- enum class DriverMode {
- // Use the fully synchronous, single-process reference driver. This driver
- // does not create any background threads and all ipcz operations will
- // complete synchronously from end-to-end.
- kSync,
+// 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 fully synchronous, single-process reference driver. This driver
+ // does not create any background threads and all ipcz operations will
+ // complete synchronously from end-to-end.
+ kSync,
- // Use the async multiprocess driver as-is. All nodes can allocate their own
- // shared memory directly through the driver.
- kAsync,
+ // Use the async multiprocess driver as-is. All nodes can allocate their own
+ // shared memory directly through the driver.
+ kAsync,
- // Use the async multiprocess driver, and force non-broker nodes to delegate
- // shared memory allocation to their broker.
- kAsyncDelegatedAlloc,
+ // Use the async multiprocess driver, and force non-broker nodes to delegate
+ // shared memory allocation to their broker.
+ kAsyncDelegatedAlloc,
- // Use the async multiprocess driver, and force non-broker-to-non-broker
- // transmission of driver objects to be relayed through a broker. All nodes
- // can allocate their own shared memory directly through the driver.
- kAsyncObjectBrokering,
+ // Use the async multiprocess driver, and force non-broker-to-non-broker
+ // transmission of driver objects to be relayed through a broker. All nodes
+ // can allocate their own shared memory directly through the driver.
+ kAsyncObjectBrokering,
- // Use the async multiprocess driver, forcing shared memory AND driver
- // object relay both to be delegated to a broker.
- kAsyncObjectBrokeringAndDelegatedAlloc,
- };
-
- MultinodeTest();
- ~MultinodeTest() override;
-
- const IpczDriver& GetDriver(DriverMode mode) const;
-
- // Creates a new broker node using the given DriverMode.
- IpczHandle CreateBrokerNode(DriverMode mode);
-
- // Creates a new broker node using the given DriverMode.
- IpczHandle CreateNonBrokerNode(DriverMode mode);
-
- // Creates a pair of transports for the given driver mode.
- void CreateBrokerToNonBrokerTransports(
- DriverMode mode,
- IpczDriverHandle* broker_transport,
- IpczDriverHandle* non_broker_transport);
-
- std::pair<IpczHandle, IpczHandle> ConnectBrokerToNonBroker(
- DriverMode mode,
- IpczHandle broker_node,
- IpczHandle non_broker_node);
+ // Use the async multiprocess driver, forcing shared memory AND driver
+ // object relay both to be delegated to a broker.
+ kAsyncObjectBrokeringAndDelegatedAlloc,
};
-// Helper for a MultinodeTest parameterized over DriverMode. Most integration
-// tests should use this for parameterization.
-class MultinodeTestWithDriver
- : public MultinodeTest,
- public testing::WithParamInterface<MultinodeTest::DriverMode> {
+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:
- const IpczDriver& GetDriver() const {
- return MultinodeTest::GetDriver(GetParam());
+ // 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);
}
- IpczHandle CreateBrokerNode() {
- return MultinodeTest::CreateBrokerNode(GetParam());
+ // 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);
}
- IpczHandle CreateNonBrokerNode() {
- return MultinodeTest::CreateNonBrokerNode(GetParam());
+ // 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;
}
- void CreateBrokerToNonBrokerTransports(
- IpczDriverHandle* broker_transport,
- IpczDriverHandle* non_broker_transport) {
- MultinodeTest::CreateBrokerToNonBrokerTransports(
- GetParam(), broker_transport, non_broker_transport);
+ // 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);
}
- std::pair<IpczHandle, IpczHandle> ConnectBrokerToNonBroker(
- IpczHandle broker_node,
- IpczHandle non_broker_node) {
- return MultinodeTest::ConnectBrokerToNonBroker(GetParam(), broker_node,
- non_broker_node);
+ // 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();
+
+ 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_;
+};
+
+// 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
+// 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; \
+ }; \
+ void node_name::NodeBody()
+
// 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::MultinodeTest::DriverMode::kSync))
+ INSTANTIATE_TEST_SUITE_P(, suite, \
+ ::testing::Values(ipcz::test::DriverMode::kSync))
#endif // IPCZ_SRC_TEST_MULTINODE_TEST_H_
diff --git a/src/test/test_base.cc b/src/test/test_base.cc
index db98271..5bc6674 100644
--- a/src/test/test_base.cc
+++ b/src/test/test_base.cc
@@ -43,7 +43,7 @@
ASSERT_EQ(IPCZ_RESULT_OK, ipcz().Close(handle, IPCZ_NO_FLAGS, nullptr));
}
-void TestBase::CloseAll(const std::vector<IpczHandle>& handles) {
+void TestBase::CloseAll(absl::Span<const IpczHandle> handles) {
for (IpczHandle handle : handles) {
Close(handle);
}
@@ -155,7 +155,15 @@
return Get(portal, message, handles);
}
-void TestBase::VerifyEndToEnd(IpczHandle a, IpczHandle b) {
+void TestBase::VerifyEndToEnd(IpczHandle portal) {
+ static const char kTestMessage[] = "Ping!!!";
+ std::string message;
+ EXPECT_EQ(IPCZ_RESULT_OK, Put(portal, kTestMessage));
+ EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(portal, &message));
+ EXPECT_EQ(kTestMessage, message);
+}
+
+void TestBase::VerifyEndToEndLocal(IpczHandle a, IpczHandle b) {
const std::string kMessage1 = "psssst";
const std::string kMessage2 = "ssshhh";
diff --git a/src/test/test_base.h b/src/test/test_base.h
index 14dbc84..b9e7cc1 100644
--- a/src/test/test_base.h
+++ b/src/test/test_base.h
@@ -8,7 +8,6 @@
#include <functional>
#include <string_view>
#include <utility>
-#include <vector>
#include "ipcz/ipcz.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -18,6 +17,12 @@
// Base class for ipcz unit tests (see ipcz::test::Test in test.h) and multinode
// test fixtures (see ipcz::test::MultinodeTest in multinode_test.h).
+//
+// Test fixtures should never derive from this class directly. For unit tests,
+// use ipcz::test::Test as a base. For multinode tests, use ipcz::test:TestNode
+// as a base for MULTINODE_TEST_NODE() invocations, and use
+// ipcz::test::MultinodeTest<T> (where T is a subclass of TestNode) for
+// TEST_P() invocations for parameterized multinode test bodies.
class TestBase {
public:
using TrapEventHandler = std::function<void(const IpczTrapEvent&)>;
@@ -27,34 +32,67 @@
const IpczAPI& ipcz() const { return ipcz_; }
- // Some shorthand methods to access the ipcz API more conveniently.
+ // Some trivial shorthand methods to access the ipcz API more conveniently.
void Close(IpczHandle handle);
- void CloseAll(const std::vector<IpczHandle>& handles);
+ void CloseAll(absl::Span<const IpczHandle> handles);
IpczHandle CreateNode(const IpczDriver& driver,
IpczCreateNodeFlags flags = IPCZ_NO_FLAGS);
std::pair<IpczHandle, IpczHandle> OpenPortals(IpczHandle node);
IpczResult Put(IpczHandle portal,
std::string_view message,
absl::Span<IpczHandle> handles = {});
+
+ // Shorthand for ipcz Get() to retrieve the next available parcel from
+ // `portal`.If no parcel is available, or any other condition prevents Get()
+ // from succeeding, this returns the same result as the ipcz Get() API.
+ //
+ // Assuming a parcel is available:
+ //
+ // If the parcel has data, it's stored as a string in `*message` iff
+ // `message` is non-null. Any handles are stored in `handles` if it's large
+ // enough to hold all handles in the parcel.
+ //
+ // If the available parcel has data but `message` is null, or if the parcel
+ // carries has more handles than the capacity of `handles`, the parcel is
+ // not retrieved, and this returns IPCZ_RESULT_RESOURCE_EXHAUSTED, like the
+ // ipcz Get() API itself.
IpczResult Get(IpczHandle portal,
std::string* message = nullptr,
absl::Span<IpczHandle> handles = {});
+
+ // Shorthand for the icpz Trap() API with convenient lambda support.
IpczResult Trap(IpczHandle portal,
const IpczTrapConditions& conditions,
TrapEventHandler fn,
IpczTrapConditionFlags* flags = nullptr,
IpczPortalStatus* status = nullptr);
+
+ // Blocks until one or more conditions indicated by `conditions` are met by
+ // `portal`. For simple flag-only conditions like peer closure,
+ // WaitForConditionFlags() below may be used instead.
IpczResult WaitForConditions(IpczHandle portal,
const IpczTrapConditions& conditions);
+
+ // Blocks until one or more conditions indicated by `flags` are met by
+ // `portal`. For parameterized conditions, use WaitForConditions() above.
IpczResult WaitForConditionFlags(IpczHandle portal,
IpczTrapConditionFlags flags);
+
+ // Waits to receive any parcel on portal.
IpczResult WaitToGet(IpczHandle portal,
std::string* message = nullptr,
absl::Span<IpczHandle> handles = {});
- // Sends a parcel from each of two portals and waits for them to be received
- // by each other.
- void VerifyEndToEnd(IpczHandle a, IpczHandle b);
+ // Sends a parcel from `portal` and expects to receive a parcel back with the
+ // same contents. Typical usage is to call this from two different nodes, on
+ // a pair of connected portals, in order to verify working communication
+ // between them.
+ void VerifyEndToEnd(IpczHandle portal);
+
+ // Similar to above, but useful in unit tests, or situations where both
+ // portals are local to the same node. In this case, a message is put into
+ // both `a` and `b`, and then this waits to read the same message from both.
+ void VerifyEndToEndLocal(IpczHandle a, IpczHandle b);
private:
static void HandleEvent(const IpczTrapEvent* event);