text: /**
   * Starts up a sharded cluster with the given specifications. The cluster
   * will be fully operational after the execution of this constructor function.
   *
   * @param {Object} params Contains the key-value pairs for the cluster
   *   configuration. Accepted keys are:
   *
   *   {
   *     name {string}: name for this test
   *     verbose {number}: the verbosity for the mongos
   *     keyFile {string}: the location of the keyFile
   *     chunksize {number}:
   *     nopreallocj {boolean|number}:
   *
   *     mongos {number|Object|Array.<Object>}: number of mongos or mongos
   *       configuration object(s)(*). @see MongoRunner.runMongos
   *
   *     rs {Object|Array.<Object>}: replica set configuration object. Can
   *       contain:
   *       {
   *         nodes {number}: number of replica members. Defaults to 3.
   *         protocolVersion {number}: protocol version of replset used by the
   *             replset initiation.
   *         initiateTimeout {number}: timeout in milliseconds to specify
   *              to ReplSetTest.prototype.initiate().
   *         For other options, @see ReplSetTest#start
   *       }
   *
   *     shards {number|Object|Array.<Object>}: number of shards or shard
   *       configuration object(s)(*). @see MongoRunner.runMongod
   *
   *     config {number|Object|Array.<Object>}: number of config server or
   *       config server configuration object(s)(*). If this field has 3 or
   *       more members, it implies other.sync = true. @see MongoRunner.runMongod
   *
   *     (*) There are two ways For multiple configuration objects.
   *       (1) Using the object format. Example:
   *
   *           { d0: { verbose: 5 }, d1: { auth: '' }, rs2: { oplogsize: 10 }}
   *
   *           In this format, d = mongod, s = mongos & c = config servers
   *
   *       (2) Using the array format. Example:
   *
   *           [{ verbose: 5 }, { auth: '' }]
   *
   *       Note: you can only have single server shards for array format.
   *
   *       Note: A special "bridgeOptions" property can be specified in both the object and array
   *          formats to configure the options for the mongobridge corresponding to that node. These
   *          options are merged with the params.bridgeOptions options, where the node-specific
   *          options take precedence.
   *
   *     other: {
   *       nopreallocj: same as above
   *       rs: same as above
   *       chunksize: same as above
   *
   *       shardOptions {Object}: same as the shards property above.
   *          Can be used to specify options that are common all shards.
   *
   *       sync {boolean}: Use SyncClusterConnection, and readies
   *          1 or 3 config servers, based on the value of numConfigs.
   *       configOptions {Object}: same as the config property above.
   *          Can be used to specify options that are common all config servers.
   *       mongosOptions {Object}: same as the mongos property above.
   *          Can be used to specify options that are common all mongos.
   *       enableBalancer {boolean} : if true, enable the balancer
   *       manualAddShard {boolean}: shards will not be added if true.
   *
   *       useBridge {boolean}: If true, then a mongobridge process is started for each node in the
   *          sharded cluster. Defaults to false.
   *
   *       bridgeOptions {Object}: Options to apply to all mongobridge processes. Defaults to {}.
   *
   *       // replica Set only:
   *       rsOptions {Object}: same as the rs property above. Can be used to
   *         specify options that are common all replica members.
   *       useHostname {boolean}: if true, use hostname of machine,
   *         otherwise use localhost
   *       numReplicas {number}
   *     }
   *   }
   *
   * Member variables:
   * s {Mongo} - connection to the first mongos
   * s0, s1, ... {Mongo} - connection to different mongos
   * rs0, rs1, ... {ReplSetTest} - test objects to replica sets
   * shard0, shard1, ... {Mongo} - connection to shards (not available for replica sets)
   * d0, d1, ... {Mongo} - same as shard0, shard1, ...
   * config0, config1, ... {Mongo} - connection to config servers
   * c0, c1, ... {Mongo} - same as config0, config1, ...
   * configRS - If the config servers are a replset, this will contain the config ReplSetTest object
   */
  var ShardingTest = function(params) {
  
      if (!(this instanceof ShardingTest)) {
          return new ShardingTest(params);
      }
  
      // Capture the 'this' reference
      var self = this;
  
      // Used for counting the test duration
      var _startTime = new Date();
  
      // Populated with the paths of all shard hosts (config servers + hosts) and is used for
      // cleaning up the data files on shutdown
      var _alldbpaths = [];
  
      // Publicly exposed variables
  
      /**
       * Attempts to open a connection to the specified connection string or throws if unable to
       * connect.
       */
      function _connectWithRetry(url) {
          var conn;
          assert.soon(function() {
              try {
                  conn = new Mongo(url);
                  return true;
              } catch (e) {
                  print("Error connecting to " + url + ": " + e);
                  return false;
              }
          });
  
          return conn;
      }
  
      /**
       * Constructs a human-readable string representing a chunk's range.
       */
      function _rangeToString(r) {
          return tojsononeline(r.min) + " -> " + tojsononeline(r.max);
      }
  
      /**
       * Checks whether the specified collection is sharded by consulting the config metadata.
       */
      function _isSharded(collName) {
          var collName = "" + collName;
          var dbName;
  
          if (typeof collName.getCollectionNames == 'function') {
              dbName = "" + collName;
              collName = undefined;
          }
  
          if (dbName) {
              var x = self.config.databases.findOne({_id: dbname});
              if (x)
                  return x.partitioned;
              else
                  return false;
          }
  
          if (collName) {
              var x = self.config.collections.findOne({_id: collName});
              if (x)
                  return true;
              else
                  return false;
          }
      }
  
      // ShardingTest API
  
      this.getRSEntry = function(setName) {
          for (var i = 0; i < this._rs.length; i++)
              if (this._rs[i].setName == setName)
                  return this._rs[i];
          throw Error("can't find rs: " + setName);
      };
  
      this.getDB = function(name) {
          return this.s.getDB(name);
      };
  
      /*
       * Gets the _id of the primary shard of the database 'dbname', e.g., 'test-rs0'
       */
      this.getServerName = function(dbname) {
          var x = this.config.databases.findOne({_id: "" + dbname});
          if (x) {
              return x.primary;
          }
  
          var countDBsFound = 0;
          this.config.databases.find().forEach(function(db) {
              countDBsFound++;
              printjson(db);
          });
          throw Error("couldn't find dbname: " + dbname + " total: " + countDBsFound);
      };
  
      this.getNonPrimaries = function(dbname) {
          var x = this.config.databases.findOne({_id: dbname});
          if (!x) {
              this.config.databases.find().forEach(printjson);
              throw Error("couldn't find dbname: " + dbname + " total: " +
                          this.config.databases.count());
          }
  
          return this.config.shards.find({_id: {$ne: x.primary}}).map(z => z._id);
      };
  
      this.getConnNames = function() {
          var names = [];
          for (var i = 0; i < this._connections.length; i++) {
              names.push(this._connections[i].name);
          }
          return names;
      };
  
      this.getServer = function(dbname) {
          var name = this.getServerName(dbname);
  
          var x = this.config.shards.findOne({_id: name});
          if (x)
              name = x.host;
  
          var rsName = null;
          if (name.indexOf("/") > 0)
              rsName = name.substring(0, name.indexOf("/"));
  
          for (var i = 0; i < this._connections.length; i++) {
              var c = this._connections[i];
              if (connectionURLTheSame(name, c.name) || connectionURLTheSame(rsName, c.name))
                  return c;
          }
  
          throw Error("can't find server for: " + dbname + " name:" + name);
      };
  
      this.normalize = function(x) {
          var z = this.config.shards.findOne({host: x});
          if (z)
              return z._id;
          return x;
      };
  
      this.getOther = function(one) {
          if (this._connections.length < 2) {
              throw Error("getOther only works with 2 servers");
          }
  
          if (one._mongo) {
              one = one._mongo;
          }
  
          for (var i = 0; i < this._connections.length; i++) {
              if (this._connections[i] != one) {
                  return this._connections[i];
              }
          }
  
          return null;
      };
  
      this.getAnother = function(one) {
          if (this._connections.length < 2) {
              throw Error("getAnother() only works with multiple servers");
          }
  
          if (one._mongo) {
              one = one._mongo;
          }
  
          for (var i = 0; i < this._connections.length; i++) {
              if (this._connections[i] == one)
                  return this._connections[(i + 1) % this._connections.length];
          }
      };
  
      this.getFirstOther = function(one) {
          for (var i = 0; i < this._connections.length; i++) {
              if (this._connections[i] != one) {
                  return this._connections[i];
              }
          }
  
          throw Error("impossible");
      };
  
      this.stopAllMongos = function(opts) {
          for (var i = 0; i < this._mongos.length; i++) {
              this.stopMongos(i);
          }
      };
  
      this.stop = function(opts) {
          this.stopAllMongos(opts);
  
          for (var i = 0; i < this._connections.length; i++) {
              if (this._rs[i]) {
                  this._rs[i].test.stopSet(15);
              } else {
                  this.stopMongod(i);
              }
          }
  
          if (this.configRS) {
              this.configRS.stopSet();
          } else {
              // Old style config triplet
              for (var i = 0; i < this._configServers.length; i++) {
                  this.stopConfigServer(i);
              }
          }
  
          for (var i = 0; i < _alldbpaths.length; i++) {
              resetDbpath(MongoRunner.dataPath + _alldbpaths[i]);
          }
  
          var timeMillis = new Date().getTime() - _startTime.getTime();
  
          print('*** ShardingTest ' + this._testName + " completed successfully in " +
                (timeMillis / 1000) + " seconds ***");
      };
  
      this.adminCommand = function(cmd) {
          var res = this.admin.runCommand(cmd);
          if (res && res.ok == 1)
              return true;
  
          throw _getErrorWithCode(res, "command " + tojson(cmd) + " failed: " + tojson(res));
      };
  
      this.printChangeLog = function() {
          this.config.changelog.find().forEach(function(z) {
              var msg = z.server + "\t" + z.time + "\t" + z.what;
              for (var i = z.what.length; i < 15; i++)
                  msg += " ";
  
              msg += " " + z.ns + "\t";
              if (z.what == "split") {
                  msg += _rangeToString(z.details.before) + " -->> (" +
                      _rangeToString(z.details.left) + "), (" + _rangeToString(z.details.right) + ")";
              } else if (z.what == "multi-split") {
                  msg += _rangeToString(z.details.before) + "  -->> (" + z.details.number + "/" +
                      z.details.of + " " + _rangeToString(z.details.chunk) + ")";
              } else {
                  msg += tojsononeline(z.details);
              }
  
              print("ShardingTest " + msg);
          });
      };
  
      this.getChunksString = function(ns) {
          var q = {};
          if (ns) {
              q.ns = ns;
          }
  
          var s = "";
          this.config.chunks.find(q)
              .sort({ns: 1, min: 1})
              .forEach(function(z) {
                  s += "  " + z._id + "\t" + z.lastmod.t + "|" + z.lastmod.i + "\t" + tojson(z.min) +
                      " -> " + tojson(z.max) + " " + z.shard + "  " + z.ns + "\n";
              });
  
          return s;
      };
  
      this.printChunks = function(ns) {
          print("ShardingTest " + this.getChunksString(ns));
      };
  
      this.printShardingStatus = function() {
          printShardingStatus(this.config);
      };
  
      this.printCollectionInfo = function(ns, msg) {
          var out = "";
          if (msg) {
              out += msg + "\n";
          }
          out += "sharding collection info: " + ns + "\n";
  
          for (var i = 0; i < this._connections.length; i++) {
              var c = this._connections[i];
              out += "  mongod " + c + " " +
                  tojson(c.getCollection(ns).getShardVersion(), " ", true) + "\n";
          }
  
          for (var i = 0; i < this._mongos.length; i++) {
              var c = this._mongos[i];
              out += "  mongos " + c + " " +
                  tojson(c.getCollection(ns).getShardVersion(), " ", true) + "\n";
          }
  
          out += this.getChunksString(ns);
  
          print("ShardingTest " + out);
      };
  
      this.sync = function() {
          this.adminCommand("connpoolsync");
      };
  
      this.onNumShards = function(collName, dbName) {
          dbName = dbName || "test";
  
          // We should sync since we're going directly to mongod here
          this.sync();
  
          var num = 0;
          for (var i = 0; i < this._connections.length; i++) {
              if (this._connections[i].getDB(dbName).getCollection(collName).count() > 0) {
                  num++;
              }
          }
  
          return num;
      };
  
      this.shardCounts = function(collName, dbName) {
          dbName = dbName || "test";
  
          // We should sync since we're going directly to mongod here
          this.sync();
  
          var counts = {};
          for (var i = 0; i < this._connections.length; i++) {
              counts[i] = this._connections[i].getDB(dbName).getCollection(collName).count();
          }
  
          return counts;
      };
  
      this.chunkCounts = function(collName, dbName) {
          dbName = dbName || "test";
  
          var x = {};
          this.config.shards.find().forEach(function(z) {
              x[z._id] = 0;
          });
  
          this.config.chunks.find({ns: dbName + "." + collName})
              .forEach(function(z) {
                  if (x[z.shard])
                      x[z.shard]++;
                  else
                      x[z.shard] = 1;
              });
  
          return x;
      };
  
      this.chunkDiff = function(collName, dbName) {
          var c = this.chunkCounts(collName, dbName);
  
          var min = 100000000;
          var max = 0;
          for (var s in c) {
              if (c[s] < min)
                  min = c[s];
              if (c[s] > max)
                  max = c[s];
          }
  
          print("ShardingTest input: " + tojson(c) + " min: " + min + " max: " + max);
          return max - min;
      };
  
      /**
       * Waits up to one minute for the difference in chunks between the most loaded shard and least
       * loaded shard to be 0 or 1, indicating that the collection is well balanced. This should only
       * be called after creating a big enough chunk difference to trigger balancing.
       */
      this.awaitBalance = function(collName, dbName, timeToWait) {
          timeToWait = timeToWait || 60000;
  
          assert.soon(function() {
              var x = self.chunkDiff(collName, dbName);
              print("chunk diff: " + x);
              return x < 2;
          }, "no balance happened", timeToWait);
      };
  
      this.getShardNames = function() {
          var shards = [];
          this.s.getCollection("config.shards")
              .find()
              .forEach(function(shardDoc) {
                  shards.push(shardDoc._id);
              });
          return shards;
      };
  
      this.getShard = function(coll, query, includeEmpty) {
          var shards = this.getShardsForQuery(coll, query, includeEmpty);
          assert.eq(shards.length, 1);
          return shards[0];
      };
  
      /**
       * Returns the shards on which documents matching a particular query reside.
       */
      this.getShardsForQuery = function(coll, query, includeEmpty) {
          if (!coll.getDB) {
              coll = this.s.getCollection(coll);
          }
  
          var explain = coll.find(query).explain("executionStats");
          var shards = [];
  
          var execStages = explain.executionStats.executionStages;
          var plannerShards = explain.queryPlanner.winningPlan.shards;
  
          if (execStages.shards) {
              for (var i = 0; i < execStages.shards.length; i++) {
                  var hasResults = execStages.shards[i].executionStages.nReturned &&
                      execStages.shards[i].executionStages.nReturned > 0;
                  if (includeEmpty || hasResults) {
                      shards.push(plannerShards[i].connectionString);
                  }
              }
          }
  
          for (var i = 0; i < shards.length; i++) {
              for (var j = 0; j < this._connections.length; j++) {
                  if (connectionURLTheSame(this._connections[j], shards[i])) {
                      shards[i] = this._connections[j];
                      break;
                  }
              }
          }
  
          return shards;
      };
  
      this.shardColl = function(collName, key, split, move, dbName, waitForDelete) {
          split = (split != false ? (split || key) : split);
          move = (split != false && move != false ? (move || split) : false);
  
          if (collName.getDB)
              dbName = "" + collName.getDB();
          else
              dbName = dbName || "test";
  
          var c = dbName + "." + collName;
          if (collName.getDB) {
              c = "" + collName;
          }
  
          var isEmpty = (this.s.getCollection(c).count() == 0);
  
          if (!_isSharded(dbName)) {
              this.s.adminCommand({enableSharding: dbName});
          }
  
          var result = this.s.adminCommand({shardcollection: c, key: key});
          if (!result.ok) {
              printjson(result);
              assert(false);
          }
  
          if (split == false) {
              return;
          }
  
          result = this.s.adminCommand({split: c, middle: split});
          if (!result.ok) {
              printjson(result);
              assert(false);
          }
  
          if (move == false) {
              return;
          }
  
          var result;
          for (var i = 0; i < 5; i++) {
              result = this.s.adminCommand({
                  movechunk: c,
                  find: move,
                  to: this.getOther(this.getServer(dbName)).name,
                  _waitForDelete: waitForDelete
              });
              if (result.ok)
                  break;
  
              sleep(5 * 1000);
          }
  
          printjson(result);
          assert(result.ok);
      };
  
      this.stopBalancer = function(timeout, interval) {
          if (typeof db == "undefined") {
              db = undefined;
          }
  
          var oldDB = db;
          db = this.config;
  
          try {
              sh.setBalancerState(false);
              sh.waitForBalancer(false, timeout, interval);
          } finally {
              db = oldDB;
          }
      };
  
      this.startBalancer = function(timeout, interval) {
          if (typeof db == "undefined") {
              db = undefined;
          }
  
          var oldDB = db;
          db = this.config;
  
          try {
              sh.setBalancerState(true);
              sh.waitForBalancer(true, timeout, interval);
          } finally {
              db = oldDB;
          }
      };
  
      /*
       * Returns true after the balancer has completed a balancing round.
       *
       * Checks that three pings were sent to config.mongos. The balancer writes a ping
       * at the start and end of a balancing round. If the balancer is in the middle of
       * a round, there could be three pings before the first full balancing round
       * completes: end ping of a round, and start and end pings of the following round.
       */
      this.waitForBalancerRound = function() {
          if (typeof db == "undefined") {
              db = undefined;
          }
          var oldDB = db;
          db = this.config;
  
          var getPings = function() {
              return sh._getConfigDB().mongos.find().toArray();
          };
  
          try {
              // If sh.waitForPingChange returns a non-empty array, config.mongos
              // was not successfully updated and no balancer round was reported.
              for (var i = 0; i < 3; ++i) {
                  if (sh.waitForPingChange(getPings()).length != 0) {
                      return false;
                  }
              }
  
              db = oldDB;
              return true;
          } catch (e) {
              print("Error running waitForPingChange: " + tojson(e));
              db = oldDB;
              return false;
          }
      };
  
      this.isAnyBalanceInFlight = function() {
          if (this.config.locks.find({_id: {$ne: "balancer"}, state: 2}).count() > 0)
              return true;
  
          var allCurrent = this.s.getDB("admin").currentOp().inprog;
          for (var i = 0; i < allCurrent.length; i++) {
              if (allCurrent[i].desc && allCurrent[i].desc.indexOf("cleanupOldData") == 0)
                  return true;
          }
          return false;
      };
  
      /*
       * Converts a CSRS connection string to an SCCC connection string.
       * TODO: Handle numbers of CSRS config servers other than 3.
       *
       * For example:
       * "test-configRS/host:20002,host:20003,host:20004" -> "host:20002,host:20003,host:20004"
       */
      this.generateSCCCFromCSRSConnectionString = function(csrsString) {
          assert(this.isCSRSConnectionString(csrsString));
  
          var numNodes = csrsString.split(",").length;
          if (numNodes !== 3) {
              throw new Error("generateSCCCFromCSRSConnectionString() expects " +
                              "3-node config servers, got " + tojson(csrsString));
          }
  
          var scccString = csrsString.substring(csrsString.indexOf("/") + 1);
          assert(this.isSCCCConnectionString(scccString));
          return scccString;
      };
  
      /*
       * Checks if connString is in the format of an SCCC connection string.
       * SCCC config servers must have exactly 3 nodes.
       *
       * Valid example:
       * "host:20002,host:20003,host:20004"
       */
      this.isSCCCConnectionString = function(connString) {
          // Should not contain a replica set name.
          assert(connString.indexOf('/') < 0);
  
          var nodes = connString.split(',');
          assert(nodes.length == 3);
  
          for (var i = 0; i < nodes.length; i++) {
              assert(nodes[i].indexOf(':') >= 0);
          }
          return true;
      };
  
      /*
       * Checks if connString is in the format of a CSRS connection string.
       * CSRS connection strings must contain a replica set name.
       *
       * Valid examples:
       * 1 node: "test-configRS/host:20002"
       * 3 node: "test-configRS/host:20002,host:20003,host:20004"
       * 4 nodes: "test-configRS/host:20002,host:20003,host:20004,host:20005"
       */
      this.isCSRSConnectionString = function(connString) {
          // Should contain a replica set name.
          assert(connString.indexOf('/') >= 0);
  
          var nodes = connString.split(',');
          assert(nodes.length > 0);
          for (var i = 0; i < nodes.length; i++) {
              assert(nodes[i].indexOf(':') >= 0);
          }
          return true;
      };
  
      /**
       * Kills the mongos with index n.
       */
      this.stopMongos = function(n) {
          if (otherParams.useBridge) {
              MongoRunner.stopMongos(unbridgedMongos[n]);
              this["s" + n].stop();
          } else {
              MongoRunner.stopMongos(this["s" + n]);
          }
      };
  
      /**
       * Kills the shard mongod with index n.
       */
      this.stopMongod = function(n) {
          if (otherParams.useBridge) {
              MongoRunner.stopMongod(unbridgedConnections[n]);
              this["d" + n].stop();
          } else {
              MongoRunner.stopMongod(this["d" + n]);
          }
      };
  
      /**
       * Kills the config server mongod with index n.
       */
      this.stopConfigServer = function(n) {
          if (otherParams.useBridge) {
              MongoRunner.stopMongod(unbridgedConfigServers[n]);
              this._configServers[n].stop();
          } else {
              MongoRunner.stopMongod(this._configServers[n]);
          }
      };
  
      /**
       * Stops and restarts a mongos process.
       *
       * If opts is specified, the new mongos is started using those options. Otherwise, it is started
       * with its previous parameters.
       *
       * Warning: Overwrites the old s (if n = 0) admin, config, and sn member variables.
       */
      this.restartMongos = function(n, opts) {
          var mongos;
  
          if (otherParams.useBridge) {
              mongos = unbridgedMongos[n];
          } else {
              mongos = this["s" + n];
          }
  
          opts = opts || mongos;
          opts.port = opts.port || mongos.port;
  
          this.stopMongos(n);
  
          if (otherParams.useBridge) {
              var bridgeOptions =
                  (opts !== mongos) ? opts.bridgeOptions : mongos.fullOptions.bridgeOptions;
              bridgeOptions = Object.merge(otherParams.bridgeOptions, bridgeOptions || {});
              bridgeOptions = Object.merge(
                  bridgeOptions,
                  {
                    hostName: otherParams.useHostname ? hostName : "localhost",
                    port: this._mongos[n].port,
                    // The mongos processes identify themselves to mongobridge as host:port, where the
                    // host is the actual hostname of the machine and not localhost.
                    dest: hostName + ":" + opts.port,
                  });
  
              this._mongos[n] = new MongoBridge(bridgeOptions);
          }
  
          var newConn = MongoRunner.runMongos(opts);
          if (!newConn) {
              throw new Error("Failed to restart mongos " + n);
          }
  
          if (otherParams.useBridge) {
              this._mongos[n].connectToBridge();
              unbridgedMongos[n] = newConn;
          } else {
              this._mongos[n] = newConn;
          }
  
          this['s' + n] = this._mongos[n];
          if (n == 0) {
              this.s = this._mongos[n];
              this.admin = this._mongos[n].getDB('admin');
              this.config = this._mongos[n].getDB('config');
          }
      };
  
      /**
       * Stops and restarts a shard mongod process.
       *
       * If opts is specified, the new mongod is started using those options. Otherwise, it is started
       * with its previous parameters.
       *
       * Warning: Overwrites the old dn/shardn member variables.
       */
      this.restartMongod = function(n) {
          var mongod;
  
          if (otherParams.useBridge) {
              mongod = unbridgedConnections[n];
          } else {
              mongod = this["d" + n];
          }
  
          this.stopMongod(n);
  
          if (otherParams.useBridge) {
              var bridgeOptions =
                  Object.merge(otherParams.bridgeOptions, mongod.fullOptions.bridgeOptions || {});
              bridgeOptions = Object.merge(
                  bridgeOptions,
                  {
                    hostName: otherParams.useHostname ? hostName : "localhost",
                    port: this._connections[n].port,
                    // The mongod processes identify themselves to mongobridge as host:port, where the
                    // host is the actual hostname of the machine and not localhost.
                    dest: hostName + ":" + mongod.port,
                  });
  
              this._connections[n] = new MongoBridge(bridgeOptions);
          }
  
          mongod.restart = true;
          var newConn = MongoRunner.runMongod(mongod);
          if (!newConn) {
              throw new Error("Failed to restart shard " + n);
          }
  
          if (otherParams.useBridge) {
              this._connections[n].connectToBridge();
              unbridgedConnections[n] = newConn;
          } else {
              this._connections[n] = newConn;
          }
  
          this["shard" + n] = this._connections[n];
          this["d" + n] = this._connections[n];
      };
  
      /**
       * Stops and restarts a config server mongod process.
       *
       * If opts is specified, the new mongod is started using those options. Otherwise, it is started
       * with its previous parameters.
       *
       * Warning: Overwrites the old cn/confign member variables.
       */
      this.restartConfigServer = function(n) {
          var mongod;
  
          if (otherParams.useBridge) {
              mongod = unbridgedConfigServers[n];
          } else {
              mongod = this["c" + n];
          }
  
          this.stopConfigServer(n);
  
          if (otherParams.useBridge) {
              var bridgeOptions =
                  Object.merge(otherParams.bridgeOptions, mongod.fullOptions.bridgeOptions || {});
              bridgeOptions = Object.merge(
                  bridgeOptions,
                  {
                    hostName: otherParams.useHostname ? hostName : "localhost",
                    port: this._configServers[n].port,
                    // The mongod processes identify themselves to mongobridge as host:port, where the
                    // host is the actual hostname of the machine and not localhost.
                    dest: hostName + ":" + mongod.port,
                  });
  
              this._configServers[n] = new MongoBridge(bridgeOptions);
          }
  
          mongod.restart = true;
          var newConn = MongoRunner.runMongod(mongod);
          if (!newConn) {
              throw new Error("Failed to restart config server " + n);
          }
  
          if (otherParams.useBridge) {
              this._configServers[n].connectToBridge();
              unbridgedConfigServers[n] = newConn;
          } else {
              this._configServers[n] = newConn;
          }
  
          this["config" + n] = this._configServers[n];
          this["c" + n] = this._configServers[n];
      };
  
      /**
       * Helper method for setting primary shard of a database and making sure that it was successful.
       * Note: first mongos needs to be up.
       */
      this.ensurePrimaryShard = function(dbName, shardName) {
          var db = this.s0.getDB('admin');
          var res = db.adminCommand({movePrimary: dbName, to: shardName});
          assert(res.ok || res.errmsg == "it is already the primary", tojson(res));
      };
  
      // ShardingTest initialization
  
      assert(isObject(params), 'ShardingTest configuration must be a JSON object');
  
      var testName = params.name || "test";
      var otherParams = Object.merge(params, params.other || {});
  
      var numShards = otherParams.hasOwnProperty('shards') ? otherParams.shards : 2;
      var verboseLevel = otherParams.hasOwnProperty('verbose') ? otherParams.verbose : 1;
      var numMongos = otherParams.hasOwnProperty('mongos') ? otherParams.mongos : 1;
      var numConfigs = otherParams.hasOwnProperty('config') ? otherParams.config : 3;
  
      // Default enableBalancer to false.
      otherParams.enableBalancer =
          ("enableBalancer" in otherParams) && (otherParams.enableBalancer === true);
  
      // Let autosplit behavior match that of the balancer if autosplit is not explicitly set.
      if (!("enableAutoSplit" in otherParams)) {
          otherParams.enableAutoSplit = otherParams.enableBalancer;
      }
  
      // Allow specifying mixed-type options like this:
      // { mongos : [ { noprealloc : "" } ],
      //   config : [ { smallfiles : "" } ],
      //   shards : { rs : true, d : true } }
      if (Array.isArray(numShards)) {
          for (var i = 0; i < numShards.length; i++) {
              otherParams["d" + i] = numShards[i];
          }
  
          numShards = numShards.length;
      } else if (isObject(numShards)) {
          var tempCount = 0;
          for (var i in numShards) {
              otherParams[i] = numShards[i];
              tempCount++;
          }
  
          numShards = tempCount;
      }
  
      if (Array.isArray(numMongos)) {
          for (var i = 0; i < numMongos.length; i++) {
              otherParams["s" + i] = numMongos[i];
          }
  
          numMongos = numMongos.length;
      } else if (isObject(numMongos)) {
          var tempCount = 0;
          for (var i in numMongos) {
              otherParams[i] = numMongos[i];
              tempCount++;
          }
  
          numMongos = tempCount;
      }
  
      if (Array.isArray(numConfigs)) {
          for (var i = 0; i < numConfigs.length; i++) {
              otherParams["c" + i] = numConfigs[i];
          }
  
          numConfigs = numConfigs.length;
      } else if (isObject(numConfigs)) {
          var tempCount = 0;
          for (var i in numConfigs) {
              otherParams[i] = numConfigs[i];
              tempCount++;
          }
  
          numConfigs = tempCount;
      }
  
      otherParams.extraOptions = otherParams.extraOptions || {};
      otherParams.useHostname = otherParams.useHostname == undefined ? true : otherParams.useHostname;
      otherParams.useBridge = otherParams.useBridge || false;
      otherParams.bridgeOptions = otherParams.bridgeOptions || {};
  
      var keyFile = otherParams.keyFile || otherParams.extraOptions.keyFile;
      var hostName = getHostName();
  
      this._testName = testName;
      this._otherParams = otherParams;
  
      var pathOpts = {
          testName: testName
      };
  
      for (var k in otherParams) {
          if (k.startsWith("rs") && otherParams[k] != undefined) {
              break;
          }
      }
  
      this._connections = [];
      this._rs = [];
      this._rsObjects = [];
  
      if (otherParams.useBridge) {
          var unbridgedConnections = [];
          var unbridgedConfigServers = [];
          var unbridgedMongos = [];
      }
  
      // Start the MongoD servers (shards)
      for (var i = 0; i < numShards; i++) {
          if (otherParams.rs || otherParams["rs" + i]) {
              var setName = testName + "-rs" + i;
  
              var rsDefaults = {
                  useHostname: otherParams.useHostname,
                  noJournalPrealloc: otherParams.nopreallocj,
                  oplogSize: 16,
                  pathOpts: Object.merge(pathOpts, {shard: i})
              };
  
              rsDefaults = Object.merge(rsDefaults, otherParams.rs);
              rsDefaults = Object.merge(rsDefaults, otherParams.rsOptions);
              rsDefaults = Object.merge(rsDefaults, otherParams["rs" + i]);
              rsDefaults.nodes = rsDefaults.nodes || otherParams.numReplicas;
              var rsSettings = rsDefaults.settings;
              delete rsDefaults.settings;
  
              var numReplicas = rsDefaults.nodes || 3;
              delete rsDefaults.nodes;
  
              var protocolVersion = rsDefaults.protocolVersion;
              delete rsDefaults.protocolVersion;
              var initiateTimeout = rsDefaults.initiateTimeout;
              delete rsDefaults.initiateTimeout;
  
              var rs = new ReplSetTest({
                  name: setName,
                  nodes: numReplicas,
                  useHostName: otherParams.useHostname,
                  useBridge: otherParams.useBridge,
                  bridgeOptions: otherParams.bridgeOptions,
                  keyFile: keyFile,
                  protocolVersion: protocolVersion,
                  shardSvr: true,
                  settings: rsSettings
              });
  
              this._rs[i] = {
                  setName: setName,
                  test: rs,
                  nodes: rs.startSet(rsDefaults),
                  url: rs.getURL()
              };
  
              rs.initiate(null, null, initiateTimeout);
  
              this["rs" + i] = rs;
              this._rsObjects[i] = rs;
  
              _alldbpaths.push(null);
              this._connections.push(null);
  
              if (otherParams.useBridge) {
                  unbridgedConnections.push(null);
              }
          } else {
              var options = {
                  useHostname: otherParams.useHostname,
                  noJournalPrealloc: otherParams.nopreallocj,
                  pathOpts: Object.merge(pathOpts, {shard: i}),
                  dbpath: "$testName$shard",
                  keyFile: keyFile
              };
  
              if (otherParams.shardOptions && otherParams.shardOptions.binVersion) {
                  otherParams.shardOptions.binVersion =
                      MongoRunner.versionIterator(otherParams.shardOptions.binVersion);
              }
  
              options = Object.merge(options, otherParams.shardOptions);
              options = Object.merge(options, otherParams["d" + i]);
  
              options.port = options.port || allocatePort();
  
              if (otherParams.useBridge) {
                  var bridgeOptions =
                      Object.merge(otherParams.bridgeOptions, options.bridgeOptions || {});
                  bridgeOptions = Object.merge(
                      bridgeOptions,
                      {
                        hostName: otherParams.useHostname ? hostName : "localhost",
                        // The mongod processes identify themselves to mongobridge as host:port, where
                        // the host is the actual hostname of the machine and not localhost.
                        dest: hostName + ":" + options.port,
                      });
  
                  var bridge = new MongoBridge(bridgeOptions);
              }
  
              var conn = MongoRunner.runMongod(options);
              if (!conn) {
                  throw new Error("Failed to start shard " + i);
              }
  
              if (otherParams.useBridge) {
                  bridge.connectToBridge();
                  this._connections.push(bridge);
                  unbridgedConnections.push(conn);
              } else {
                  this._connections.push(conn);
              }
  
              _alldbpaths.push(testName + i);
              this["shard" + i] = this._connections[i];
              this["d" + i] = this._connections[i];
  
              this._rs[i] = null;
              this._rsObjects[i] = null;
          }
      }
  
      // Do replication on replica sets if required
      for (var i = 0; i < numShards; i++) {
          if (!otherParams.rs && !otherParams["rs" + i]) {
              continue;
          }
  
          var rs = this._rs[i].test;
          rs.getPrimary().getDB("admin").foo.save({x: 1});
  
          if (keyFile) {
              authutil.asCluster(rs.nodes,
                                 keyFile,
                                 function() {
                                     rs.awaitReplication();
                                 });
          }
  
          rs.awaitSecondaryNodes();
  
          var rsConn = new Mongo(rs.getURL());
          rsConn.name = rs.getURL();
  
          this._connections[i] = rsConn;
          this["shard" + i] = rsConn;
          rsConn.rs = rs;
      }
  
      // Default to using 3-node legacy config servers if jsTestOptions().useLegacyOptions is true
      // and the user didn't explicity specify a different config server configuration
      if (jsTestOptions().useLegacyConfigServers && otherParams.sync !== false &&
          (typeof(otherParams.config) === 'undefined' || numConfigs === 3)) {
          otherParams.sync = true;
      }
  
      this._configServers = [];
  
      // Start the config servers
      if (otherParams.sync) {
          if (numConfigs !== 1 && numConfigs !== 3) {
              throw Error('Sync config servers only supported with 1 or 3 nodes');
          }
  
          var configNames = [];
          for (var i = 0; i < numConfigs; i++) {
              var options = {
                  useHostname: otherParams.useHostname,
                  noJournalPrealloc: otherParams.nopreallocj,
                  pathOpts: Object.merge(pathOpts, {config: i}),
                  dbpath: "$testName-config$config",
                  keyFile: keyFile,
                  // Ensure that journaling is always enabled for config servers.
                  journal: "",
                  configsvr: ""
              };
  
              if (otherParams.configOptions && otherParams.configOptions.binVersion) {
                  otherParams.configOptions.binVersion =
                      MongoRunner.versionIterator(otherParams.configOptions.binVersion);
              }
  
              options = Object.merge(options, otherParams.configOptions);
              options = Object.merge(options, otherParams["c" + i]);
  
              options.port = options.port || allocatePort();
  
              if (otherParams.useBridge) {
                  var bridgeOptions =
                      Object.merge(otherParams.bridgeOptions, options.bridgeOptions || {});
                  bridgeOptions = Object.merge(
                      bridgeOptions,
                      {
                        hostName: otherParams.useHostname ? hostName : "localhost",
                        // The mongod processes identify themselves to mongobridge as host:port, where
                        // the host is the actual hostname of the machine and not localhost.
                        dest: hostName + ":" + options.port,
                      });
  
                  var bridge = new MongoBridge(bridgeOptions);
              }
  
              var conn = MongoRunner.runMongod(options);
              if (!conn) {
                  throw new Error("Failed to start config server " + i);
              }
  
              if (otherParams.useBridge) {
                  bridge.connectToBridge();
                  this._configServers.push(bridge);
                  unbridgedConfigServers.push(conn);
                  configNames.push(bridge.host);
              } else {
                  this._configServers.push(conn);
                  configNames.push(conn.name);
              }
  
              _alldbpaths.push(testName + "-config" + i);
              this["config" + i] = this._configServers[i];
              this["c" + i] = this._configServers[i];
          }
  
          this._configDB = configNames.join(',');
      } else {
          // Using replica set for config servers
          var rstOptions = {
              useHostName: otherParams.useHostname,
              useBridge: otherParams.useBridge,
              bridgeOptions: otherParams.bridgeOptions,
              keyFile: keyFile,
              name: testName + "-configRS",
          };
  
          // when using CSRS, always use wiredTiger as the storage engine
          var startOptions = {
              pathOpts: pathOpts,
              // Ensure that journaling is always enabled for config servers.
              journal: "",
              configsvr: "",
              noJournalPrealloc: otherParams.nopreallocj,
              storageEngine: "wiredTiger",
          };
  
          if (otherParams.configOptions && otherParams.configOptions.binVersion) {
              otherParams.configOptions.binVersion =
                  MongoRunner.versionIterator(otherParams.configOptions.binVersion);
          }
  
          startOptions = Object.merge(startOptions, otherParams.configOptions);
          rstOptions = Object.merge(rstOptions, otherParams.configReplSetTestOptions);
  
          var nodeOptions = [];
          for (var i = 0; i < numConfigs; ++i) {
              nodeOptions.push(otherParams["c" + i] || {});
          }
  
          rstOptions.nodes = nodeOptions;
  
          this.configRS = new ReplSetTest(rstOptions);
          this.configRS.startSet(startOptions);
  
          var config = this.configRS.getReplSetConfig();
          config.configsvr = true;
          config.settings = config.settings || {};
          var initiateTimeout = otherParams.rsOptions && otherParams.rsOptions.initiateTimeout;
          this.configRS.initiate(config, null, initiateTimeout);
  
          this.configRS.getPrimary();  // Wait for master to be elected before starting mongos
  
          this._configDB = this.configRS.getURL();
          this._configServers = this.configRS.nodes;
          for (var i = 0; i < numConfigs; ++i) {
              var conn = this._configServers[i];
              this["config" + i] = conn;
              this["c" + i] = conn;
          }
      }
  
      printjson("config servers: " + this._configDB);
  
      var configConnection = _connectWithRetry(this._configDB);
  
      print("ShardingTest " + this._testName + " :\n" +
            tojson({config: this._configDB, shards: this._connections}));
  
      if (numMongos == 0 && !otherParams.noChunkSize) {
          if (keyFile) {
              throw Error("Cannot set chunk size without any mongos when using auth");
          } else {
              configConnection.getDB("config").settings.insert(
                  {_id: "chunksize", value: otherParams.chunksize || otherParams.chunkSize || 50});
          }
      }
  
      this._mongos = [];
  
      // Start the MongoS servers
      for (var i = 0; i < ((numMongos == 0 ? -1 : numMongos) || 1); i++) {
          options = {
              useHostname: otherParams.useHostname,
              pathOpts: Object.merge(pathOpts, {mongos: i}),
              configdb: this._configDB,
              verbose: verboseLevel || 0,
              keyFile: keyFile,
          };
  
          if (!otherParams.noChunkSize) {
              options.chunkSize = otherParams.chunksize || otherParams.chunkSize || 50;
          }
  
          if (otherParams.mongosOptions && otherParams.mongosOptions.binVersion) {
              otherParams.mongosOptions.binVersion =
                  MongoRunner.versionIterator(otherParams.mongosOptions.binVersion);
          }
  
          options = Object.merge(options, otherParams.mongosOptions);
          options = Object.merge(options, otherParams.extraOptions);
          options = Object.merge(options, otherParams["s" + i]);
  
          options.port = options.port || allocatePort();
  
          // Disable autosplitting unless it is explicitly turned on.
          if (!otherParams.enableAutoSplit) {
              options.noAutoSplit = "";
          }
  
          if (otherParams.useBridge) {
              var bridgeOptions =
                  Object.merge(otherParams.bridgeOptions, options.bridgeOptions || {});
              bridgeOptions = Object.merge(
                  bridgeOptions,
                  {
                    hostName: otherParams.useHostname ? hostName : "localhost",
                    // The mongos processes identify themselves to mongobridge as host:port, where the
                    // host is the actual hostname of the machine and not localhost.
                    dest: hostName + ":" + options.port,
                  });
  
              var bridge = new MongoBridge(bridgeOptions);
          }
  
          var conn = MongoRunner.runMongos(options);
          if (!conn) {
              throw new Error("Failed to start mongos " + i);
          }
  
          if (otherParams.useBridge) {
              bridge.connectToBridge();
              this._mongos.push(bridge);
              unbridgedMongos.push(conn);
          } else {
              this._mongos.push(conn);
          }
  
          if (i === 0) {
              this.s = this._mongos[i];
              this.admin = this._mongos[i].getDB('admin');
              this.config = this._mongos[i].getDB('config');
          }
  
          this["s" + i] = this._mongos[i];
      }
  
      // If auth is enabled for the test, login the mongos connections as system in order to
      // configure the instances and then log them out again.
      if (keyFile) {
          authutil.assertAuthenticate(this._mongos,
                                      'admin',
                                      {
                                        user: '__system',
                                        mechanism: 'MONGODB-CR',
                                        pwd: cat(keyFile).replace(/[\011-\015\040]/g, '')
                                      });
      }
  
      try {
          // Disable the balancer unless it is explicitly turned on
          if (!otherParams.enableBalancer) {
              this.stopBalancer();
          }
  
          // Lower the mongos replica set monitor's threshold for deeming RS shard hosts as
          // inaccessible in order to speed up tests, which shutdown entire shards and check for
          // errors. This attempt is best-effort and failure should not have effect on the actual
          // test execution, just the execution time.
          this._mongos.forEach(function(mongos) {
              var res = mongos.adminCommand({setParameter: 1, replMonitorMaxFailedChecks: 2});
  
              // For tests, which use x509 certificate for authentication, the command above will not
              // work due to authorization error.
              if (res.code != ErrorCodes.Unauthorized) {
                  assert.commandWorked(res);
              }
          });
      } finally {
          if (keyFile) {
              authutil.logout(this._mongos, 'admin');
          }
      }
  
      if (!otherParams.manualAddShard) {
          this._shardNames = [];
  
          var testName = this._testName;
          var admin = this.admin;
          var shardNames = this._shardNames;
  
          this._connections.forEach(function(z) {
              var n = z.name;
              if (!n) {
                  n = z.host;
                  if (!n) {
                      n = z;
                  }
              }
  
              print("ShardingTest " + testName + " going to add shard : " + n);
  
              var result = admin.runCommand({addshard: n});
              assert.commandWorked(result, "Failed to add shard " + n);
  
              shardNames.push(result.shardAdded);
              z.shardName = result.shardAdded;
          });
      }
  
      if (jsTestOptions().keyFile) {
          jsTest.authenticate(configConnection);
          jsTest.authenticateNodes(this._configServers);
          jsTest.authenticateNodes(this._mongos);
      }
  };
  