summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSiyuan Zhou <siyuan.zhou@mongodb.com>2020-07-13 03:58:12 -0400
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-11-03 23:06:07 +0000
commitb907dabad7724cce00695d74cdd0823756b24d05 (patch)
treeaa852dbfdafa8997fcb74b80d52248d182822367
parent3a62d9a1f02e5d7e83285712584b97d941882f70 (diff)
downloadmongo-b907dabad7724cce00695d74cdd0823756b24d05.tar.gz
SERVER-50154 Add declarative mock network framework for unit testing.
-rw-r--r--src/mongo/db/repl/SConscript1
-rw-r--r--src/mongo/db/repl/initial_syncer_test.cpp97
-rw-r--r--src/mongo/db/repl/mock_fixture.cpp135
-rw-r--r--src/mongo/db/repl/mock_fixture.h155
-rw-r--r--src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp74
-rw-r--r--src/mongo/executor/network_interface_mock.cpp12
-rw-r--r--src/mongo/executor/network_interface_mock.h6
7 files changed, 480 insertions, 0 deletions
diff --git a/src/mongo/db/repl/SConscript b/src/mongo/db/repl/SConscript
index 10f68f9bad4..b03690d1ba3 100644
--- a/src/mongo/db/repl/SConscript
+++ b/src/mongo/db/repl/SConscript
@@ -785,6 +785,7 @@ env.Library(
env.Library(
target='replmocks',
source=[
+ 'mock_fixture.cpp',
'replication_consistency_markers_mock.cpp',
'replication_coordinator_external_state_mock.cpp',
'replication_coordinator_mock.cpp',
diff --git a/src/mongo/db/repl/initial_syncer_test.cpp b/src/mongo/db/repl/initial_syncer_test.cpp
index fd33e1d9899..077f346373f 100644
--- a/src/mongo/db/repl/initial_syncer_test.cpp
+++ b/src/mongo/db/repl/initial_syncer_test.cpp
@@ -46,6 +46,7 @@
#include "mongo/db/repl/data_replicator_external_state_mock.h"
#include "mongo/db/repl/initial_syncer.h"
#include "mongo/db/repl/member_state.h"
+#include "mongo/db/repl/mock_fixture.h"
#include "mongo/db/repl/oplog_entry.h"
#include "mongo/db/repl/oplog_fetcher.h"
#include "mongo/db/repl/oplog_fetcher_mock.h"
@@ -106,6 +107,7 @@ namespace {
using namespace mongo;
using namespace mongo::repl;
+using namespace mongo::test::mock;
using executor::NetworkInterfaceMock;
using executor::RemoteCommandRequest;
@@ -1713,6 +1715,42 @@ TEST_F(InitialSyncerTest, InitialSyncerPassesThroughFCVFetcherScheduleError) {
assertFCVRequest(request);
}
+// This is to demonstrate the unit testing mock framework. The logic is the same as the following
+// test.
+TEST_F(InitialSyncerTest, InitialSyncerPassesThroughFCVFetcherCallbackError_Mock) {
+ auto initialSyncer = &getInitialSyncer();
+ auto opCtx = makeOpCtx();
+
+ _syncSourceSelector->setChooseNewSyncSourceResult_forTest(HostAndPort("localhost", 12345));
+
+ MockNetwork mock(getNet());
+
+ // Set up default behavior.
+ mock.expect("replSetGetRBID", makeRollbackCheckerResponse(1));
+
+ mock.expect([](auto& request) { return request["find"].str() == "oplog.rs"; },
+ makeCursorResponse(0LL, _options.localOplogNS, {makeOplogEntryObj(1)}));
+
+ mock.expect(
+ [](auto& request) { return request["find"].str() == "transactions"; },
+ makeCursorResponse(0LL, NamespaceString::kSessionTransactionsTableNamespace, {}, true));
+
+ // This is what we want to test.
+ mock.expect([](auto& request) { return request["find"].str() == "system.version"; },
+ RemoteCommandResponse(ErrorCodes::OperationFailed,
+ "find command failed at sync source"))
+ .times(1);
+
+ // Start the real work.
+ ASSERT_OK(initialSyncer->startup(opCtx.get(), maxAttempts));
+
+ // Run mock.
+ mock.runUntilExpectationsSatisfied();
+
+ initialSyncer->join();
+ ASSERT_EQUALS(ErrorCodes::OperationFailed, _lastApplied);
+}
+
TEST_F(InitialSyncerTest, InitialSyncerPassesThroughFCVFetcherCallbackError) {
auto initialSyncer = &getInitialSyncer();
auto opCtx = makeOpCtx();
@@ -1943,6 +1981,65 @@ TEST_F(InitialSyncerTest, InitialSyncerSucceedsWhenFCVFetcherReturnsOldVersion)
ASSERT_EQUALS(ErrorCodes::CallbackCanceled, _lastApplied);
}
+// This is to demonstrate the unit testing mock framework. The logic is the same as the following
+// test.
+TEST_F(
+ InitialSyncerTest,
+ InitialSyncerPassesThroughOplogFetcherRestartsBasedOnInitialSyncFetcherRestartDecision_Mock) {
+ auto initialSyncer = &getInitialSyncer();
+ auto opCtx = makeOpCtx();
+
+ _syncSourceSelector->setChooseNewSyncSourceResult_forTest(HostAndPort("localhost", 12345));
+
+ const std::uint32_t initialSyncMaxAttempts = 2U;
+
+ auto lastOp = makeOplogEntry(2);
+
+ MockNetwork mock(getNet());
+
+ // Set up default behavior.
+ mock.expect("replSetGetRBID", makeRollbackCheckerResponse(1));
+
+ mock.expect([](auto& request) { return request["find"].str() == "oplog.rs"; },
+ makeCursorResponse(0LL, _options.localOplogNS, {makeOplogEntryObj(1)}))
+ .times(2);
+
+ mock.expect([](auto& request) { return request["find"].str() == "transactions"; },
+ makeCursorResponse(0LL, NamespaceString::kSessionTransactionsTableNamespace, {}));
+
+ // This is what we want to test.
+ FeatureCompatibilityVersionDocument fcvDoc;
+ // (Generic FCV reference): This FCV reference should exist across LTS binary versions.
+ fcvDoc.setVersion(ServerGlobalParams::FeatureCompatibility::kLastLTS);
+ mock.expect([](auto& request) { return request["find"].str() == "system.version"; },
+ makeCursorResponse(
+ 0LL, NamespaceString::kServerConfigurationNamespace, {fcvDoc.toBSON()}))
+ .times(1);
+
+ FailPointEnableBlock skipReconstructPreparedTransactions("skipReconstructPreparedTransactions");
+ FailPointEnableBlock skipRecoverTenantMigrationAccessBlockers(
+ "skipRecoverTenantMigrationAccessBlockers");
+
+ // Start the real work.
+ ASSERT_OK(initialSyncer->startup(opCtx.get(), initialSyncMaxAttempts));
+
+ mock.runUntilExpectationsSatisfied();
+
+ // Simulate response to OplogFetcher so it has enough operations to reach end timestamp.
+ getOplogFetcher()->receiveBatch(1LL, {makeOplogEntryObj(1), lastOp.toBSON()});
+ // Simulate a network error response that restarts the OplogFetcher.
+ getOplogFetcher()->simulateResponseError(Status(ErrorCodes::NetworkTimeout, "network error"));
+
+ mock.expect([](auto& request) { return request["find"].str() == "oplog.rs"; },
+ makeCursorResponse(0LL, _options.localOplogNS, {lastOp.toBSON()}))
+ .times(1);
+
+ mock.runUntilExpectationsSatisfied();
+
+ initialSyncer->join();
+ ASSERT_OK(_lastApplied.getStatus());
+}
+
TEST_F(InitialSyncerTest,
InitialSyncerPassesThroughOplogFetcherRestartsBasedOnInitialSyncFetcherRestartDecision) {
auto initialSyncer = &getInitialSyncer();
diff --git a/src/mongo/db/repl/mock_fixture.cpp b/src/mongo/db/repl/mock_fixture.cpp
new file mode 100644
index 00000000000..2d4156fdca9
--- /dev/null
+++ b/src/mongo/db/repl/mock_fixture.cpp
@@ -0,0 +1,135 @@
+/**
+ * Copyright (C) 2020-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/db/repl/mock_fixture.h"
+
+#include "mongo/executor/network_interface_mock.h"
+#include "mongo/logv2/log.h"
+
+namespace mongo {
+namespace test {
+namespace mock {
+
+bool MockNetwork::_allExpectationsSatisfied() const {
+ return std::all_of(_expectations.begin(), _expectations.end(), [](const Expectation& exp) {
+ return exp.isDefault() || exp.isSatisfied();
+ });
+}
+
+void MockNetwork::_runUntilIdle() {
+ executor::NetworkInterfaceMock::InNetworkGuard guard(_net);
+ do {
+ // The main responsibility of the mock network is to host incoming requests and scheduled
+ // responses. Additionally, the mock network interface is a de facto lock-step scheduler.
+ //
+ // The executor thread and the mock/test thread run in turn. The test thread
+ // (1) triggers the tested behavior, e.g. by simulating a command;
+ // (2) responds to network requests;
+ // (3) advances the mock clock; and
+ // (4) handles some network operations implicitly (explained below).
+ // The executor runs the asynchronous jobs which may schedule network requests.
+ //
+ // The executor thread gets the first turn, then each of them yields at the end of their
+ // turns by enabling and signaling the other. The executor thread yields by calling
+ // waitForWork() on the mock network; the test thread yields by calling
+ // runReadyNetworkOperations().
+ //
+ // runReadyNetworkOperations() also first checks for expired scheduled works (e.g. request
+ // timeout) and executes the expired works. This behavior is the item (4) mentioned above.
+ //
+ // After yielding to the executor thread, it's possible that new expired scheduled works
+ // were added by the executor. That's why we need to double check if there's any ready
+ // network operations before deciding the network is idle.
+ //
+ // External threads may make things more complex. For example, they can schedule new
+ // requests right after we thought the network was idle. However, that's always the case
+ // with or without the mock framework.
+ _net->runReadyNetworkOperations();
+ if (_net->hasReadyRequests()) {
+ // Peek the next request.
+ auto noi = _net->getFrontOfUnscheduledQueue();
+ auto request = noi->getRequest().cmdObj;
+
+ // We ignore the next request if it's not expected.
+ auto exp = std::find_if(_expectations.begin(), _expectations.end(), [&](auto& exp) {
+ return !exp.isSatisfied() && exp.match(request);
+ });
+
+ if (exp != _expectations.end()) {
+ // Consume the next request and execute the action.
+ noi = _net->getNextReadyRequest();
+ auto response = exp->run(request);
+ LOGV2_DEBUG(5015401,
+ 1,
+ "mock reply ",
+ "request"_attr = request,
+ "response"_attr = response);
+ _net->scheduleResponse(noi, _net->now(), response);
+
+ // Continue handling network operations and process requests.
+ continue;
+ }
+ }
+
+ // The executor is idle since we just ran it. Check hasReadyNetworkOperations so that no
+ // scheduled work is waiting for the network thread.
+ } while (_net->hasReadyNetworkOperations());
+}
+
+void MockNetwork::runUntilExpectationsSatisfied() {
+ // If there exist extra threads beside the executor and the mock/test thread, when the
+ // network is idle, the extra threads may be running and will schedule new requests. As a
+ // result, the current best practice is to busy-loop to prepare for that.
+ while (!_allExpectationsSatisfied()) {
+ _runUntilIdle();
+ }
+}
+
+void MockNetwork::runUntil(Date_t target) {
+ while (_net->now() < target) {
+ LOGV2_DEBUG(
+ 5015402, 1, "mock advances time", "from"_attr = _net->now(), "to"_attr = target);
+ {
+ executor::NetworkInterfaceMock::InNetworkGuard guard(_net);
+ // Even if we cannot reach target time, we are still making progress in the loop.
+ _net->runUntil(target);
+ }
+ // Run until idle.
+ _runUntilIdle();
+ }
+ LOGV2_DEBUG(5015403, 1, "mock reached time", "target"_attr = target);
+}
+
+} // namespace mock
+} // namespace test
+} // namespace mongo
diff --git a/src/mongo/db/repl/mock_fixture.h b/src/mongo/db/repl/mock_fixture.h
new file mode 100644
index 00000000000..c0da17ae456
--- /dev/null
+++ b/src/mongo/db/repl/mock_fixture.h
@@ -0,0 +1,155 @@
+/**
+ * Copyright (C) 2020-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#pragma once
+
+#include <limits>
+
+#include "mongo/executor/network_interface_mock.h"
+#include "mongo/stdx/thread.h"
+
+namespace mongo {
+
+class BSONObj;
+using executor::RemoteCommandResponse;
+
+namespace test {
+namespace mock {
+
+// MockNetwork wraps the NetworkInterfaceMock to provide a declarative approach
+// to specify expected behaviors on the network and to hide the interaction with
+// the NetworkInterfaceMock.
+class MockNetwork {
+public:
+ using MatcherFunc = std::function<bool(const BSONObj&)>;
+ using ActionFunc = std::function<RemoteCommandResponse(const BSONObj&)>;
+
+ class Matcher {
+ public:
+ Matcher(const char* cmdName) : Matcher(std::string(cmdName)) {}
+ Matcher(const std::string& cmdName) {
+ _matcherFunc = [=](const BSONObj& request) {
+ return request.firstElementFieldNameStringData() == cmdName;
+ };
+ }
+
+ Matcher(MatcherFunc matcherFunc) : _matcherFunc(std::move(matcherFunc)) {}
+
+ bool operator()(const BSONObj& request) {
+ return _matcherFunc(request);
+ }
+
+ private:
+ MatcherFunc _matcherFunc;
+ };
+
+ class Action {
+ public:
+ Action(ActionFunc func) : _actionFunc(std::move(func)){};
+
+ Action(const BSONObj& response) {
+ _actionFunc = [=](const BSONObj& request) {
+ return RemoteCommandResponse(response, Milliseconds(0));
+ };
+ }
+
+ Action(const RemoteCommandResponse& commandResponse) {
+ _actionFunc = [=](const BSONObj& request) { return commandResponse; };
+ }
+
+ RemoteCommandResponse operator()(const BSONObj& request) {
+ return _actionFunc(request);
+ }
+
+ private:
+ ActionFunc _actionFunc;
+ };
+
+ class Expectation {
+ public:
+ Expectation(Matcher matcher, Action action)
+ : _matcher(std::move(matcher)), _action(std::move(action)) {}
+
+ Expectation& times(int t) {
+ _allowedTimes = t;
+ return *this;
+ }
+
+ bool match(const BSONObj& request) {
+ return _matcher(request);
+ }
+
+ RemoteCommandResponse run(const BSONObj& request) {
+ if (!isDefault()) {
+ _allowedTimes--;
+ }
+ return _action(request);
+ }
+
+ bool isDefault() const {
+ return _allowedTimes == std::numeric_limits<int>::max();
+ }
+
+ bool isSatisfied() const {
+ return _allowedTimes == 0;
+ }
+
+ private:
+ Matcher _matcher;
+ Action _action;
+ int _allowedTimes = std::numeric_limits<int>::max();
+ };
+
+ explicit MockNetwork(executor::NetworkInterfaceMock* net) : _net(net) {}
+
+ // Accept anything that Matcher's and Action's constructors allow.
+ template <typename MatcherType>
+ Expectation& expect(MatcherType&& matcher, Action action) {
+ _expectations.emplace_back(Matcher(std::forward<MatcherType>(matcher)), std::move(action));
+ return _expectations.back();
+ }
+
+ // Advance time to the target. Run network operations and process requests along the way.
+ void runUntil(Date_t targetTime);
+
+ // Run until both the executor and the network are idle and all expectations are satisfied.
+ // Otherwise, it hangs forever.
+ void runUntilExpectationsSatisfied();
+
+private:
+ void _runUntilIdle();
+ bool _allExpectationsSatisfied() const;
+
+ std::vector<Expectation> _expectations;
+ executor::NetworkInterfaceMock* _net;
+};
+
+} // namespace mock
+} // namespace test
+} // namespace mongo
diff --git a/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp b/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp
index 360cfb72bb8..66c87ba8eac 100644
--- a/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp
+++ b/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp
@@ -35,6 +35,7 @@
#include "mongo/db/jsobj.h"
#include "mongo/db/operation_context_noop.h"
#include "mongo/db/repl/is_master_response.h"
+#include "mongo/db/repl/mock_fixture.h"
#include "mongo/db/repl/repl_set_config.h"
#include "mongo/db/repl/repl_set_heartbeat_args_v1.h"
#include "mongo/db/repl/repl_set_heartbeat_response.h"
@@ -56,6 +57,8 @@ namespace mongo {
namespace repl {
namespace {
+using namespace mongo::test::mock;
+
using executor::NetworkInterfaceMock;
using executor::RemoteCommandRequest;
using executor::RemoteCommandResponse;
@@ -339,6 +342,77 @@ TEST_F(ReplCoordTest, ElectionSucceedsWhenMaxSevenNodesVoteYea) {
ASSERT_EQUALS(1, countTextFormatLogLinesContaining("Election succeeded"));
}
+// This is to demonstrate the unit testing mock framework. The logic is the same as the following
+// test.
+TEST_F(ReplCoordTest, ElectionFailsWhenInsufficientVotesAreReceivedDuringDryRun_Mock) {
+ startCapturingLogMessages();
+ BSONObj configObj = BSON("_id"
+ << "mySet"
+ << "version" << 1 << "members"
+ << BSON_ARRAY(BSON("_id" << 1 << "host"
+ << "node1:12345")
+ << BSON("_id" << 2 << "host"
+ << "node2:12345")
+ << BSON("_id" << 3 << "host"
+ << "node3:12345"))
+ << "protocolVersion" << 1);
+ assertStartSuccess(configObj, HostAndPort("node1", 12345));
+ ReplSetConfig config = assertMakeRSConfig(configObj);
+
+ OperationContextNoop opCtx;
+ OpTime time1(Timestamp(100, 1), 0);
+ replCoordSetMyLastAppliedOpTime(time1, Date_t() + Seconds(time1.getSecs()));
+ replCoordSetMyLastDurableOpTime(time1, Date_t() + Seconds(time1.getSecs()));
+ ASSERT_OK(getReplCoord()->setFollowerMode(MemberState::RS_SECONDARY));
+
+ simulateEnoughHeartbeatsForAllNodesUp();
+
+ // Check that the node's election candidate metrics are unset before it becomes primary.
+ ASSERT_BSONOBJ_EQ(
+ BSONObj(), ReplicationMetrics::get(getServiceContext()).getElectionCandidateMetricsBSON());
+
+ auto electionTimeoutWhen = getReplCoord()->getElectionTimeout_forTest();
+ ASSERT_NOT_EQUALS(Date_t(), electionTimeoutWhen);
+ LOGV2(2145401,
+ "Election timeout scheduled at {electionTimeoutWhen} (simulator time)",
+ "electionTimeoutWhen"_attr = electionTimeoutWhen);
+
+ MockNetwork mock(getNet());
+
+ // Heartbeat default behavior.
+ OpTime lastApplied(Timestamp(100, 1), 0);
+ ReplSetHeartbeatResponse hbResp;
+ auto rsConfig = getReplCoord()->getReplicaSetConfig_forTest();
+ hbResp.setSetName(rsConfig.getReplSetName());
+ hbResp.setState(MemberState::RS_SECONDARY);
+ hbResp.setConfigVersion(rsConfig.getConfigVersion());
+ hbResp.setConfigTerm(rsConfig.getConfigTerm());
+ hbResp.setAppliedOpTimeAndWallTime({lastApplied, Date_t() + Seconds(lastApplied.getSecs())});
+ hbResp.setDurableOpTimeAndWallTime({lastApplied, Date_t() + Seconds(lastApplied.getSecs())});
+
+ mock.expect("replSetHeartbeat", hbResp.toBSON());
+
+ mock.expect("replSetRequestVotes",
+ BSON("ok" << 1 << "term" << 0 << "voteGranted" << false << "reason"
+ << "don't like him much"))
+ .times(2);
+
+ // Trigger election.
+ mock.runUntil(electionTimeoutWhen);
+
+ mock.runUntilExpectationsSatisfied();
+
+ stopCapturingLogMessages();
+ ASSERT_EQUALS(1,
+ countTextFormatLogLinesContaining(
+ "Not running for primary, we received insufficient votes"));
+
+ // Check that the node's election candidate metrics have been cleared, since it lost the dry-run
+ // election and will not become primary.
+ ASSERT_BSONOBJ_EQ(
+ BSONObj(), ReplicationMetrics::get(getServiceContext()).getElectionCandidateMetricsBSON());
+}
+
TEST_F(ReplCoordTest, ElectionFailsWhenInsufficientVotesAreReceivedDuringDryRun) {
startCapturingLogMessages();
BSONObj configObj = BSON("_id"
diff --git a/src/mongo/executor/network_interface_mock.cpp b/src/mongo/executor/network_interface_mock.cpp
index 513dcf78a63..ae0677f5ec3 100644
--- a/src/mongo/executor/network_interface_mock.cpp
+++ b/src/mongo/executor/network_interface_mock.cpp
@@ -617,6 +617,18 @@ void NetworkInterfaceMock::_runReadyNetworkOperations_inlock(stdx::unique_lock<s
_waitingToRunMask &= ~kNetworkThread;
}
+bool NetworkInterfaceMock::hasReadyNetworkOperations() {
+ stdx::lock_guard<stdx::mutex> lk(_mutex);
+ invariant(_currentlyRunning == kNetworkThread);
+ if (!_alarms.empty() && _now_inlock() >= _alarms.top().when) {
+ return true;
+ }
+ if (!_scheduled.empty() && _scheduled.front().getResponseDate() <= _now_inlock()) {
+ return true;
+ }
+ return false;
+}
+
void NetworkInterfaceMock::_waitForWork_inlock(stdx::unique_lock<stdx::mutex>* lk) {
if (_waitingToRunMask & kExecutorThread) {
_waitingToRunMask &= ~kExecutorThread;
diff --git a/src/mongo/executor/network_interface_mock.h b/src/mongo/executor/network_interface_mock.h
index 911917523fc..fdc8b5b753c 100644
--- a/src/mongo/executor/network_interface_mock.h
+++ b/src/mongo/executor/network_interface_mock.h
@@ -287,6 +287,12 @@ public:
const std::vector<NetworkOperationList*>& queuesToCheck,
const TaskExecutor::ResponseStatus& response);
+ /**
+ * Returns true if there is no scheduled work (i.e. alarms and scheduled responses) for the
+ * network thread to process.
+ */
+ bool hasReadyNetworkOperations();
+
private:
/**
* Information describing a scheduled alarm.