diff options
Diffstat (limited to 'src/mongo/executor/mock_remote_command_runner_test.cpp')
-rw-r--r-- | src/mongo/executor/mock_remote_command_runner_test.cpp | 273 |
1 files changed, 250 insertions, 23 deletions
diff --git a/src/mongo/executor/mock_remote_command_runner_test.cpp b/src/mongo/executor/mock_remote_command_runner_test.cpp index def6e7a421c..38b8dd7aa7a 100644 --- a/src/mongo/executor/mock_remote_command_runner_test.cpp +++ b/src/mongo/executor/mock_remote_command_runner_test.cpp @@ -37,19 +37,22 @@ #include "mongo/executor/remote_command_runner_test_fixture.h" #include "mongo/executor/remote_command_targeter.h" #include "mongo/unittest/unittest.h" +#include "mongo/util/debugger.h" +#include "mongo/util/optional_util.h" namespace mongo::executor::remote_command_runner { namespace { /** - * This test fixture is used to test the functionality of the mock, rather than test any facilities + * This test fixture is used to test the functionality of the mocks, rather than test any facilities * or usage of the RemoteCommandRunner implementation. */ +template <typename MockType> class MockRemoteCommandRunnerTestFixture : public RemoteCommandRunnerTestFixture { public: void setUp() override { RemoteCommandRunnerTestFixture::setUp(); - auto uniqueMock = std::make_unique<MockRemoteCommandRunner>(); + auto uniqueMock = std::make_unique<MockType>(); _mock = uniqueMock.get(); detail::RemoteCommandRunner::set(getServiceContext(), std::move(uniqueMock)); } @@ -59,40 +62,264 @@ public: RemoteCommandRunnerTestFixture::tearDown(); } - MockRemoteCommandRunner& getMockRunner() { + MockType& getMockRunner() { return *_mock; } + + SemiFuture<RemoteCommandRunnerResponse<HelloCommandReply>> sendHelloCommandToHostAndPort( + HostAndPort target) { + HelloCommand hello; + initializeCommand(hello); + auto opCtxHolder = makeOperationContext(); + return doRequest(hello, + opCtxHolder.get(), + std::make_unique<RemoteCommandFixedTargeter>(target), + getExecutorPtr(), + _cancellationToken); + } + + SemiFuture<RemoteCommandRunnerResponse<HelloCommandReply>> sendHelloCommandToLocalHost() { + return sendHelloCommandToHostAndPort({"localhost", serverGlobalParams.port}); + } + private: - MockRemoteCommandRunner* _mock; + MockType* _mock; }; +using SyncMockRemoteCommandRunnerTestFixture = + MockRemoteCommandRunnerTestFixture<SyncMockRemoteCommandRunner>; +using AsyncMockRemoteCommandRunnerTestFixture = + MockRemoteCommandRunnerTestFixture<AsyncMockRemoteCommandRunner>; + // A simple test showing that an arbitrary mock result can be set for a command scheduled through // the RemoteCommandRunner. -TEST_F(MockRemoteCommandRunnerTestFixture, Example) { - HelloCommand hello; - initializeCommand(hello); - // Doc that shouldn't be parseable as a HelloCommandReply. - auto invalidResult = BSON("An" - << "arbitrary" - << "bogus" - << "document"); - getMockRunner().setMockResult(invalidResult); - - auto opCtxHolder = makeOperationContext(); - auto res = doRequest(hello, - opCtxHolder.get(), - std::make_unique<RemoteCommandLocalHostTargeter>(), - getExecutorPtr(), - _cancellationToken); +TEST_F(SyncMockRemoteCommandRunnerTestFixture, RemoteSuccess) { + auto responseFuture = sendHelloCommandToLocalHost(); + + HelloCommandReply helloReply = HelloCommandReply(TopologyVersion(OID::gen(), 0)); + BSONObjBuilder result(helloReply.toBSON()); + CommandHelpers::appendCommandStatusNoThrow(result, Status::OK()); + auto expectedResultObj = result.obj(); + + auto& request = getMockRunner().getNextRequest(); + ASSERT_FALSE(responseFuture.isReady()); + + request.respondWith(expectedResultObj); + auto actualResult = responseFuture.get(); + HostAndPort localhost = HostAndPort("localhost", serverGlobalParams.port); + ASSERT_EQ(actualResult.targetUsed, localhost); + ASSERT_BSONOBJ_EQ(actualResult.response.toBSON(), helloReply.toBSON()); +} + +TEST_F(SyncMockRemoteCommandRunnerTestFixture, RemoteError) { + StringData exampleErrMsg{"example error message"}; + auto exampleErrCode = ErrorCodes::ShutdownInProgress; + ErrorReply errorReply; + errorReply.setOk(0); + errorReply.setCode(exampleErrCode); + errorReply.setCodeName(ErrorCodes::errorString(exampleErrCode)); + errorReply.setErrmsg(exampleErrMsg); + + auto responseFuture = sendHelloCommandToLocalHost(); + + auto& request = getMockRunner().getNextRequest(); + request.respondWith(errorReply.toBSON()); auto check = [&](const DBException& ex) { - ASSERT_EQ(ex.code(), 40415) << ex.toString(); - ASSERT_STRING_CONTAINS(ex.reason(), "is an unknown field"); + ASSERT_EQ(ex.code(), ErrorCodes::RemoteCommandExecutionError) << ex.toString(); + auto extraInfo = ex.extraInfo<RemoteCommandExecutionErrorInfo>(); + ASSERT(extraInfo); + + ASSERT(extraInfo->isRemote()); + auto remoteError = extraInfo->asRemote(); + ASSERT_EQ(remoteError.getRemoteCommandResult().code(), exampleErrCode); + ASSERT_EQ(remoteError.getRemoteCommandResult().reason(), exampleErrMsg); }; // Ensure we fail to parse the reply due to the unknown fields. - ASSERT_THROWS_WITH_CHECK(res.get(), DBException, check); + ASSERT_THROWS_WITH_CHECK(responseFuture.get(), DBException, check); +} + +TEST_F(SyncMockRemoteCommandRunnerTestFixture, LocalError) { + auto responseFuture = sendHelloCommandToLocalHost(); + auto& request = getMockRunner().getNextRequest(); + ASSERT_FALSE(responseFuture.isReady()); + auto exampleLocalErr = Status{ErrorCodes::InterruptedAtShutdown, "example local error"}; + request.respondWith(exampleLocalErr); + ASSERT_EQ(responseFuture.getNoThrow().getStatus(), exampleLocalErr); +} + +TEST_F(SyncMockRemoteCommandRunnerTestFixture, MultipleResponses) { + auto responseOneFut = sendHelloCommandToLocalHost(); + ASSERT_FALSE(responseOneFut.isReady()); + auto& request = getMockRunner().getNextRequest(); + auto responseTwoFut = sendHelloCommandToLocalHost(); + ASSERT_FALSE(responseTwoFut.isReady()); + + HelloCommandReply helloReply = HelloCommandReply(TopologyVersion(OID::gen(), 0)); + BSONObjBuilder result(helloReply.toBSON()); + CommandHelpers::appendCommandStatusNoThrow(result, Status::OK()); + auto expectedResultObj = result.obj(); + + request.respondWith(expectedResultObj); + auto responseOne = responseOneFut.get(); + HostAndPort localhost = HostAndPort("localhost", serverGlobalParams.port); + ASSERT_EQ(responseOne.targetUsed, localhost); + ASSERT_BSONOBJ_EQ(responseOne.response.toBSON(), helloReply.toBSON()); + + auto& requestTwo = getMockRunner().getNextRequest(); + ASSERT_FALSE(responseTwoFut.isReady()); + auto exampleLocalErr = Status{ErrorCodes::InterruptedAtShutdown, "example local error"}; + requestTwo.respondWith(exampleLocalErr); + ASSERT_EQ(responseTwoFut.getNoThrow().getStatus(), exampleLocalErr); +} + +TEST_F(SyncMockRemoteCommandRunnerTestFixture, OnCommand) { + auto responseFut = sendHelloCommandToLocalHost(); + + HelloCommandReply helloReply = HelloCommandReply(TopologyVersion(OID::gen(), 0)); + BSONObjBuilder result(helloReply.toBSON()); + CommandHelpers::appendCommandStatusNoThrow(result, Status::OK()); + auto expectedResultObj = result.obj(); + + HelloCommand hello; + initializeCommand(hello); + + getMockRunner().onCommand([&](const RequestInfo& ri) { + ASSERT_BSONOBJ_EQ(hello.toBSON({}), ri._cmd); + return expectedResultObj; + }); + ASSERT_BSONOBJ_EQ(responseFut.get().response.toBSON(), helloReply.toBSON()); +} + +// A simple test showing that we can asynchronously register an expectation +// that a request will eventually be scheduled with the mock before a request +// actually arrives. Then, once the request is scheduled, we are asynchronously +// notified of the request and can schedule a response to it. +TEST_F(AsyncMockRemoteCommandRunnerTestFixture, Expectation) { + // We expect that some code will use the runner to send a hello + // to localhost on "testdb" + auto matcher = [](const AsyncMockRemoteCommandRunner::Request& req) { + bool isHello = req.cmdBSON.firstElementFieldName() == "hello"_sd; + bool isRightTarget = req.target == HostAndPort("localhost", serverGlobalParams.port); + return isHello && isRightTarget; + }; + // Register our expectation and ensure it isn't yet met. + HelloCommandReply helloReply = HelloCommandReply(TopologyVersion(OID::gen(), 0)); + BSONObjBuilder result(helloReply.toBSON()); + CommandHelpers::appendCommandStatusNoThrow(result, Status::OK()); + + auto expectation = getMockRunner().expect(matcher, result.obj(), "example expectation"); + ASSERT_FALSE(expectation.isReady()); + + // Allow a request to be scheduled on the mock. + auto response = sendHelloCommandToLocalHost(); + + // Now, our expectation should be met, and the response to it provided. + auto reply = response.get(); + expectation.get(); + ASSERT_BSONOBJ_EQ(reply.response.toBSON(), helloReply.toBSON()); + ASSERT_EQ(HostAndPort("localhost", serverGlobalParams.port), reply.targetUsed); +} + +// A more complicated test that registers several expectations, and then +// schedules the requests that match them and their responses out-of-order. +// Demonstrates how we can register expectations on the mock for events in an +// unordered way. +TEST_F(AsyncMockRemoteCommandRunnerTestFixture, SeveralExpectations) { + HostAndPort targetOne("FakeHost1", 12345); + HostAndPort targetTwo("FakeHost2", 12345); + HostAndPort targetThree("FakeHost3", 12345); + + auto matcherOne = [&](const AsyncMockRemoteCommandRunner::Request& req) { + return (req.cmdBSON.firstElementFieldName() == "hello"_sd) && (req.target == targetOne); + }; + auto matcherTwo = [&](const AsyncMockRemoteCommandRunner::Request& req) { + return (req.cmdBSON.firstElementFieldName() == "hello"_sd) && (req.target == targetTwo); + }; + auto matcherThree = [&](const AsyncMockRemoteCommandRunner::Request& req) { + return (req.cmdBSON.firstElementFieldName() == "hello"_sd) && (req.target == targetThree); + }; + + // Create three expectations + HelloCommandReply helloReply = HelloCommandReply(TopologyVersion(OID::gen(), 0)); + BSONObjBuilder result(helloReply.toBSON()); + CommandHelpers::appendCommandStatusNoThrow(result, Status::OK()); + auto resultObj = result.obj(); + auto e1 = getMockRunner().expect(matcherOne, resultObj, "expectation one"); + auto e2 = getMockRunner().expect(matcherTwo, resultObj, "expectation two"); + auto e3 = getMockRunner().expect(matcherThree, resultObj, "expectation three"); + + ASSERT_FALSE(e1.isReady()); + ASSERT_FALSE(e2.isReady()); + ASSERT_FALSE(e3.isReady()); + + // Send requests corresponding to expectations `e3` and `e2`, but not as`e1`. + auto r3 = sendHelloCommandToHostAndPort(targetThree); + auto r2 = sendHelloCommandToHostAndPort(targetTwo); + e3.get(); + e2.get(); + ASSERT_FALSE(e1.isReady()); + // Make sure the correct responses were sent. + auto assertResponseMatches = [&](RemoteCommandRunnerResponse<HelloCommandReply> reply, + const HostAndPort& correctTarget) { + ASSERT_EQ(correctTarget, reply.targetUsed); + ASSERT_BSONOBJ_EQ(reply.response.toBSON(), helloReply.toBSON()); + }; + assertResponseMatches(r3.get(), targetThree); + assertResponseMatches(r2.get(), targetTwo); + + // Now, send a request matching `e1` as well. + auto r1 = sendHelloCommandToHostAndPort(targetOne); + assertResponseMatches(r1.get(), targetOne); + e1.get(); } +TEST_F(AsyncMockRemoteCommandRunnerTestFixture, UnexpectedRequests) { + auto responseFut = sendHelloCommandToLocalHost(); + ASSERT_EQ(responseFut.getNoThrow() + .getStatus() + .extraInfo<RemoteCommandExecutionErrorInfo>() + ->asLocal(), + Status(ErrorCodes::InternalErrorNotSupported, "Unexpected request")); + ASSERT(getMockRunner().hadUnexpectedRequests()); + auto unexpectedRequests = getMockRunner().getUnexpectedRequests(); + ASSERT_EQ(unexpectedRequests.size(), 1); + HelloCommand hello; + initializeCommand(hello); + ASSERT_BSONOBJ_EQ(unexpectedRequests[0].cmdBSON, hello.toBSON({})); + ASSERT_EQ(unexpectedRequests[0].dbName, "testdb"_sd); + HostAndPort localhost = HostAndPort("localhost", serverGlobalParams.port); + ASSERT_EQ(unexpectedRequests[0].target, localhost); + // Note that unexpected requests are BSON-convertable and can be printed as extended JSON. + // For example, if you wanted to fail the test if any unexpected requests were found, and + // print out the first such offending request, you could simply do: + // ASSERT(!getMockRunner().hadUnexpectedRequests()) + // << "but found: " << optional_io::Extension{getMockRunner().getFirstUnexpectedRequest()}; + // (This is a live example, feel free to uncomment and try it). +} + +TEST_F(AsyncMockRemoteCommandRunnerTestFixture, UnmetExpectations) { + HostAndPort theTarget("FakeHost1", 12345); + auto matcher = [&](const AsyncMockRemoteCommandRunner::Request& req) { + return (req.cmdBSON.firstElementFieldName() == "hello"_sd) && (req.target == theTarget); + }; + HelloCommandReply helloReply = HelloCommandReply(TopologyVersion(OID::gen(), 0)); + BSONObjBuilder result(helloReply.toBSON()); + CommandHelpers::appendCommandStatusNoThrow(result, Status::OK()); + auto resultObj = result.obj(); + auto expectation = getMockRunner().expect(matcher, resultObj, "unmet expectation"); + + ASSERT(getMockRunner().hasUnmetExpectations()); + auto unmetExpectations = getMockRunner().getUnmetExpectations(); + ASSERT_EQ(unmetExpectations.size(), 1); + ASSERT(unmetExpectations.contains("unmet expectation")); + // Note that unmet expectations all have string names and can be printed. + // For example, if you wanted to fail the test if any unmet expectations were found, and + // print out the first such offending expectation , you could simply do: + // ASSERT(!getMockRunner().hasUnmetExpectations()) + // << optional_io::Extension{getMockRunner().getFirstUnmetExpectation()}; + // (This is a live example, feel free to uncomment and try it). +} } // namespace } // namespace mongo::executor::remote_command_runner |