1""" 2Interface of the different fixtures for executing JSTests against. 3""" 4 5from __future__ import absolute_import 6 7import os.path 8import time 9 10import pymongo 11import pymongo.errors 12 13from ... import config 14from ... import errors 15from ... import logging 16from ... import utils 17from ...utils import registry 18 19 20_FIXTURES = {} 21 22 23def make_fixture(class_name, *args, **kwargs): 24 """ 25 Factory function for creating Fixture instances. 26 """ 27 28 if class_name not in _FIXTURES: 29 raise ValueError("Unknown fixture class '%s'" % (class_name)) 30 return _FIXTURES[class_name](*args, **kwargs) 31 32 33class Fixture(object): 34 """ 35 Base class for all fixtures. 36 """ 37 38 __metaclass__ = registry.make_registry_metaclass(_FIXTURES) 39 40 # We explicitly set the 'REGISTERED_NAME' attribute so that PyLint realizes that the attribute 41 # is defined for all subclasses of Fixture. 42 REGISTERED_NAME = "Fixture" 43 44 def __init__(self, logger, job_num, dbpath_prefix=None): 45 """ 46 Initializes the fixture with a logger instance. 47 """ 48 49 if not isinstance(logger, logging.Logger): 50 raise TypeError("logger must be a Logger instance") 51 52 if not isinstance(job_num, int): 53 raise TypeError("job_num must be an integer") 54 elif job_num < 0: 55 raise ValueError("job_num must be a nonnegative integer") 56 57 self.logger = logger 58 self.job_num = job_num 59 60 dbpath_prefix = utils.default_if_none(config.DBPATH_PREFIX, dbpath_prefix) 61 dbpath_prefix = utils.default_if_none(dbpath_prefix, config.DEFAULT_DBPATH_PREFIX) 62 self._dbpath_prefix = os.path.join(dbpath_prefix, "job{}".format(self.job_num)) 63 64 def setup(self): 65 """ 66 Creates the fixture. 67 """ 68 pass 69 70 def await_ready(self): 71 """ 72 Blocks until the fixture can be used for testing. 73 """ 74 pass 75 76 def teardown(self, finished=False): 77 """ 78 Destroys the fixture. Return true if was successful, and false 79 otherwise. 80 81 The fixture's logging handlers are closed if 'finished' is true, 82 which should happen when setup() won't be called again. 83 """ 84 85 try: 86 return self._do_teardown() 87 finally: 88 if finished: 89 for handler in self.logger.handlers: 90 # We ignore the cancellation token returned by close_later() since we always 91 # want the logs to eventually get flushed. 92 logging.flush.close_later(handler) 93 94 def _do_teardown(self): 95 """ 96 Destroys the fixture. Return true if was successful, and false 97 otherwise. 98 """ 99 return True 100 101 def is_running(self): 102 """ 103 Returns true if the fixture is still operating and more tests 104 can be run, and false otherwise. 105 """ 106 return True 107 108 def get_dbpath_prefix(self): 109 return self._dbpath_prefix 110 111 def get_internal_connection_string(self): 112 """ 113 Returns the connection string for this fixture. This is NOT a 114 driver connection string, but a connection string of the format 115 expected by the mongo::ConnectionString class. 116 """ 117 raise NotImplementedError("get_connection_string must be implemented by Fixture subclasses") 118 119 def get_driver_connection_url(self): 120 """ 121 Return the mongodb connection string as defined here: 122 https://docs.mongodb.com/manual/reference/connection-string/ 123 """ 124 raise NotImplementedError( 125 "get_driver_connection_url must be implemented by Fixture subclasses") 126 127 def mongo_client(self, read_preference=pymongo.ReadPreference.PRIMARY, timeout_millis=30000): 128 """ 129 Returns a pymongo.MongoClient connecting to this fixture with a read 130 preference of 'read_preference'. 131 132 The PyMongo driver will wait up to 'timeout_millis' milliseconds 133 before concluding that the server is unavailable. 134 """ 135 136 kwargs = {"connectTimeoutMS": timeout_millis} 137 if pymongo.version_tuple[0] >= 3: 138 kwargs["serverSelectionTimeoutMS"] = timeout_millis 139 kwargs["connect"] = True 140 141 return pymongo.MongoClient(host=self.get_driver_connection_url(), 142 read_preference=read_preference, 143 **kwargs) 144 145 def __str__(self): 146 return "%s (Job #%d)" % (self.__class__.__name__, self.job_num) 147 148 def __repr__(self): 149 return "%r(%r, %r)" % (self.__class__.__name__, self.logger, self.job_num) 150 151 152class ReplFixture(Fixture): 153 """ 154 Base class for all fixtures that support replication. 155 """ 156 157 REGISTERED_NAME = registry.LEAVE_UNREGISTERED 158 159 AWAIT_REPL_TIMEOUT_MINS = 5 160 161 def get_primary(self): 162 """ 163 Returns the primary of a replica set, or the master of a 164 master-slave deployment. 165 """ 166 raise NotImplementedError("get_primary must be implemented by ReplFixture subclasses") 167 168 def get_secondaries(self): 169 """ 170 Returns a list containing the secondaries of a replica set, or 171 the slave of a master-slave deployment. 172 """ 173 raise NotImplementedError("get_secondaries must be implemented by ReplFixture subclasses") 174 175 def retry_until_wtimeout(self, insert_fn): 176 """ 177 Given a callback function representing an insert operation on 178 the primary, handle any connection failures, and keep retrying 179 the operation for up to 'AWAIT_REPL_TIMEOUT_MINS' minutes. 180 181 The insert operation callback should take an argument for the 182 number of remaining seconds to provide as the timeout for the 183 operation. 184 """ 185 186 deadline = time.time() + ReplFixture.AWAIT_REPL_TIMEOUT_MINS * 60 187 188 while True: 189 try: 190 remaining = deadline - time.time() 191 insert_fn(remaining) 192 break 193 except pymongo.errors.ConnectionFailure: 194 remaining = deadline - time.time() 195 if remaining <= 0.0: 196 raise errors.ServerFailure( 197 "Failed to connect to ".format(self.get_driver_connection_url())) 198 199 200class NoOpFixture(Fixture): 201 """A Fixture implementation that does not start any servers. 202 203 Used when the MongoDB deployment is started by the JavaScript test itself with MongoRunner, 204 ReplSetTest, or ShardingTest. 205 """ 206 207 REGISTERED_NAME = "NoOpFixture" 208 209 def get_internal_connection_string(self): 210 return None 211 212 def get_driver_connection_url(self): 213 return None 214