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