diff --git a/.travis.yml b/.travis.yml index d505aab..bf969b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ language: python services: - mongodb +install: python -m pip install https://github.com/mongodb/mongo-python-driver/archive/3.11.0rc0.tar.gz + python: - 3.5 - 3.6 diff --git a/pymongoexplain/commands.py b/pymongoexplain/commands.py index 25e2ea1..4344ef7 100644 --- a/pymongoexplain/commands.py +++ b/pymongoexplain/commands.py @@ -19,16 +19,72 @@ from typing import Union from bson.son import SON +from collections import abc + from pymongo.collection import Collection +from pymongo.collation import validate_collation_or_none from .utils import convert_to_camelcase Document = Union[dict, SON] +def _index_document(index_list): + """Helper to generate an index specifying document. + + Takes a list of (key, direction) pairs. + """ + if isinstance(index_list, abc.Mapping): + raise TypeError("passing a dict to sort/create_index/hint is not " + "allowed - use a list of tuples instead. did you " + "mean %r?" % list(index_list.items())) + elif not isinstance(index_list, (list, tuple)): + raise TypeError("must use a list of (key, direction) pairs, " + "not: " + repr(index_list)) + if not len(index_list): + raise ValueError("key_or_list must not be the empty list") + + index = SON() + for (key, value) in index_list: + if not isinstance(key, str): + raise TypeError("first item in each key pair must be a string") + if not isinstance(value, (str, int, abc.Mapping)): + raise TypeError("second item in each key pair must be 1, -1, " + "'2d', 'geoHaystack', or another valid MongoDB " + "index specifier.") + index[key] = value + return index + + +def _fields_list_to_dict(fields, option_name): + """Takes a sequence of field names and returns a matching dictionary. + + ["a", "b"] becomes {"a": 1, "b": 1} + + and + + ["a.b.c", "d", "a.c"] becomes {"a.b.c": 1, "d": 1, "a.c": 1} + """ + if isinstance(fields, abc.Mapping): + return fields + + if isinstance(fields, (abc.Sequence, abc.Set)): + if not all(isinstance(field, str) for field in fields): + raise TypeError("%s must be a list of key names, each an " + "instance of %s" % (option_name, + str.__name__)) + return dict.fromkeys(fields, 1) + + raise TypeError("%s must be a mapping or " + "list of key names" % (option_name,)) + + class BaseCommand(): - def __init__(self, collection): + def __init__(self, collection, collation): self.command_document = {} + collation = validate_collation_or_none(collation) + if collation is not None: + self.command_document["collation"] = collation self.collection = collection @property @@ -44,16 +100,39 @@ def get_SON(self): class UpdateCommand(BaseCommand): def __init__(self, collection: Collection, filter, update, - kwargs): - super().__init__(collection.name) - return_document = {"updates":[{"q": filter, "u": update}]} - for key in kwargs: - value = kwargs[key] - if key == "bypass_document_validation": - return_document[key] = value - else: - return_document["updates"][0][key] = value - self.command_document = convert_to_camelcase(return_document) + upsert=None, multi=None, collation=None, array_filters=None, + hint=None, ordered=None, write_concern=None, + bypass_document_validation=None, comment=None): + super().__init__(collection.name, collation) + update_doc = {"q": filter, "u": update} + if upsert is not None: + update_doc["upsert"] = upsert + + if multi is not None: + update_doc["multi"] = multi + + if array_filters is not None: + update_doc["array_filters"] = array_filters + + if hint is not None: + update_doc["hint"] = hint if \ + isinstance(hint, str) else _index_document(hint) + self.command_document["updates"] = [update_doc] + + if ordered is not None: + self.command_document["ordered"] = ordered + + if write_concern is not None: + self.command_document["write_concern"] = write_concern + + if bypass_document_validation is not None and \ + bypass_document_validation is not False: + self.command_document["bypass_document_validation"] = bypass_document_validation + + if comment is not None: + self.command_document["comment"] = comment + + self.command_document = convert_to_camelcase(self.command_document) @property def command_name(self): @@ -61,12 +140,11 @@ def command_name(self): class DistinctCommand(BaseCommand): - def __init__(self, collection: Collection, key, filter, session, + def __init__(self, collection: Collection, key, filter, kwargs): - super().__init__(collection.name) - self.command_document = {"key": key, "query": filter} - for key, value in kwargs.items(): - self.command_document[key] = value + super().__init__(collection.name, kwargs.pop("collation", None)) + self.command_document.update({"key": key, "query": filter}) + self.command_document = convert_to_camelcase(self.command_document) @property @@ -75,26 +153,34 @@ def command_name(self): class AggregateCommand(BaseCommand): - def __init__(self, collection: Collection, pipeline, session, + def __init__(self, collection: Collection, pipeline, cursor_options, - kwargs, exclude_keys = []): - super().__init__(collection.name) - self.command_document = {"pipeline": pipeline, "cursor": cursor_options} + kwargs): + + super().__init__(collection.name, kwargs.pop("collation", None)) + self.command_document.update({"pipeline": pipeline, "cursor": + cursor_options}) + for key, value in kwargs.items(): - self.command_document[key] = value + if key == "batchSize": + if value == 0: + continue + self.command_document["cursor"]["batchSize"] = value + else: + self.command_document[key] = value self.command_document = convert_to_camelcase( - self.command_document, exclude_keys=exclude_keys) + self.command_document) @property def command_name(self): return "aggregate" + class CountCommand(BaseCommand): - def __init__(self, collection: Collection, filter, - kwargs): - super().__init__(collection.name) - self.command_document = {"query": filter} + def __init__(self, collection: Collection, filter, kwargs): + super().__init__(collection.name, kwargs.pop("collation", None)) + self.command_document.update({"query": filter}) for key, value in kwargs.items(): self.command_document[key] = value self.command_document = convert_to_camelcase(self.command_document) @@ -107,21 +193,37 @@ def command_name(self): class FindCommand(BaseCommand): def __init__(self, collection: Collection, kwargs): - super().__init__(collection.name) + super().__init__(collection.name, kwargs.pop("collation", None)) for key, value in kwargs.items(): - self.command_document[key] = value + if key == "projection" and value is not None: + self.command_document["projection"] = _fields_list_to_dict( + value, "projection") + elif key == "sort": + self.command_document["sort"] = _index_document( + value) + else: + self.command_document[key] = value + self.command_document = convert_to_camelcase(self.command_document) @property def command_name(self): return "find" + class FindAndModifyCommand(BaseCommand): def __init__(self, collection: Collection, kwargs): - super().__init__(collection.name) + super().__init__(collection.name, kwargs.pop("collation", None)) for key, value in kwargs.items(): - self.command_document[key] = value + if key == "hint": + self.command_document["hint"] = value if \ + isinstance(value, str) else _index_document(value) + elif key == "sort" and value is not None: + self.command_document["sort"] = _index_document( + value) + else: + self.command_document[key] = value self.command_document = convert_to_camelcase(self.command_document) @property @@ -132,10 +234,16 @@ def command_name(self): class DeleteCommand(BaseCommand): def __init__(self, collection: Collection, filter, limit, collation, kwargs): - super().__init__(collection.name) - self.command_document = {"deletes": [SON({"q": filter, "limit": limit})]} + super().__init__(collection.name, kwargs.pop("collation", None)) + self.command_document["deletes"] = [{"q": filter, "limit": + limit}] for key, value in kwargs.items(): - self.command_document[key] = value + if key == "hint": + self.command_document["deletes"][0]["hint"] = value if \ + isinstance(value, str) else _index_document(value) + else: + self.command_document[key] = value + self.command_document = convert_to_camelcase(self.command_document) @property diff --git a/pymongoexplain/explainable_collection.py b/pymongoexplain/explainable_collection.py index e736fd2..bf227aa 100644 --- a/pymongoexplain/explainable_collection.py +++ b/pymongoexplain/explainable_collection.py @@ -16,6 +16,7 @@ from typing import Union, List, Dict import pymongo +from pymongo.collection import Collection from bson.son import SON from .commands import AggregateCommand, FindCommand, CountCommand, \ @@ -23,6 +24,7 @@ Document = Union[dict, SON] + class ExplainCollection(): def __init__(self, collection): self.collection = collection @@ -39,31 +41,28 @@ def update_one(self, filter, update, upsert=False, bypass_document_validation=False, collation=None, array_filters=None, hint=None, session=None, **kwargs): - kwargs.update(locals()) - del kwargs["self"], kwargs["kwargs"], kwargs["filter"], kwargs["update"] - kwargs["multi"] = False - if bypass_document_validation == False: - del kwargs["bypass_document_validation"] - command = UpdateCommand(self.collection, filter, update, kwargs) + command = UpdateCommand(self.collection, filter, update, + bypass_document_validation= + bypass_document_validation, + array_filters=array_filters, + collation=collation, hint=hint, + upsert=upsert, multi=False) return self._explain_command(command) def update_many(self, filter: Document, update: Document, upsert=False, - array_filters=None, bypass_document_validation=False, collation=None, session=None, **kwargs): - kwargs.update(locals()) - del kwargs["self"], kwargs["kwargs"], kwargs["filter"], kwargs["update"] - kwargs["multi"] = True - if bypass_document_validation == False: - del kwargs["bypass_document_validation"] - command = UpdateCommand(self.collection, filter, update, kwargs) + array_filters=None, bypass_document_validation=False, + collation=None, hint=None, session=None, **kwargs): + command = UpdateCommand(self.collection, filter, update, multi=True, + bypass_document_validation=bypass_document_validation, upsert=upsert, collation=collation, array_filters=array_filters, hint=hint) return self._explain_command(command) def distinct(self, key: str, filter: Document=None, session=None, **kwargs): - command = DistinctCommand(self.collection, key, filter, session, kwargs) + command = DistinctCommand(self.collection, key, filter, kwargs) return self._explain_command(command) def aggregate(self, pipeline: List[Document], session=None, **kwargs): - command = AggregateCommand(self.collection, pipeline, session, - {},kwargs) + command = AggregateCommand(self.collection, pipeline, + {}, kwargs) return self._explain_command(command) def estimated_document_count(self, @@ -76,9 +75,8 @@ def count_documents(self, filter: Document, session=None, **kwargs): command = AggregateCommand(self.collection, [{'$match': filter}, - {'$group': {'n': {'$sum': 1}, '_id': 1}}], - session, {}, kwargs, - exclude_keys=filter.keys()) + {'$group': {'n': {'$sum': 1}, '_id': 1}}] + , {}, kwargs) return self._explain_command(command) def delete_one(self, filter: Document, collation=None, session=None, @@ -93,16 +91,14 @@ def delete_many(self, filter: Document, collation=None, Document, bool]]): limit = 0 - kwargs["session"] = session command = DeleteCommand(self.collection, filter, limit, collation, - kwargs) + kwargs) return self._explain_command(command) def watch(self, pipeline: Document = None, full_document: Document = None, resume_after= None, max_await_time_ms: int = None, batch_size: int = None, - collation=None, start_at_operation_time=None, session: - pymongo.mongo_client.client_session.ClientSession=None, + collation=None, start_at_operation_time=None, session=None, start_after=None): change_stream_options = {"start_after":start_after, "resume_after":resume_after, @@ -114,7 +110,7 @@ def watch(self, pipeline: Document = None, full_document: Document = None, pipeline = [{"$changeStream": change_stream_options}] command = AggregateCommand(self.collection, pipeline, - session, {"batch_size":batch_size}, + {"batch_size":batch_size}, {"collation":collation, "max_await_time_ms": max_await_time_ms}) return self._explain_command(command) @@ -124,7 +120,8 @@ def find(self, filter: Document = None, kwargs.update(locals()) del kwargs["self"], kwargs["kwargs"] command = FindCommand(self.collection, - kwargs) + kwargs) + return self._explain_command(command) def find_one(self, filter: Document = None, **kwargs: Dict[str, @@ -149,7 +146,8 @@ def find_one_and_delete(self, filter: Document, projection: list = None, kwargs) return self._explain_command(command) - def find_one_and_replace(self, filter: Document, replacement: Document, + def find_one_and_replace(self, filter: Document, replacement: + Document={}, projection: list = None, sort=None, return_document=pymongo.ReturnDocument.BEFORE, session=None, **kwargs): @@ -163,7 +161,7 @@ def find_one_and_replace(self, filter: Document, replacement: Document, kwargs) return self._explain_command(command) - def find_one_and_update(self, filter: Document, replacement: Document, + def find_one_and_update(self, filter: Document, update: Document, projection: list = None, sort=None, return_document=pymongo.ReturnDocument.BEFORE, session=None, **kwargs): @@ -171,7 +169,7 @@ def find_one_and_update(self, filter: Document, replacement: Document, kwargs["fields"] = projection kwargs["sort"] = sort kwargs["upsert"] = False - kwargs["update"] = replacement + kwargs["update"] = update kwargs["session"] = session command = FindAndModifyCommand(self.collection, @@ -180,15 +178,10 @@ def find_one_and_update(self, filter: Document, replacement: Document, def replace_one(self, filter: Document, replacement: Document, upsert=False, bypass_document_validation=False, - collation=None, session=None, **kwargs): - kwargs.update(locals()) - del kwargs["self"], kwargs["kwargs"], kwargs["filter"], kwargs[ - "replacement"] - kwargs["multi"] = False - if not bypass_document_validation: - del kwargs["bypass_document_validation"] - update = replacement - command = UpdateCommand(self.collection, filter, update, kwargs) + collation=None, hint=None, session=None, **kwargs): + command = UpdateCommand(self.collection, filter, update=replacement, + bypass_document_validation=bypass_document_validation, + hint=hint, collation=collation, multi=False, upsert=upsert) return self._explain_command(command) diff --git a/pymongoexplain/utils.py b/pymongoexplain/utils.py index d956a15..12c00d0 100644 --- a/pymongoexplain/utils.py +++ b/pymongoexplain/utils.py @@ -16,26 +16,17 @@ """Utility functions""" -def convert_to_camelcase(d, exclude_keys=[]): +def convert_to_camelcase(d): if not isinstance(d, dict): return d ret = dict() for key in d.keys(): if d[key] is None: continue - if key in exclude_keys: - ret[key] = d[key] - continue new_key = key if "_" in key and key[0] != "_": new_key = key.split("_")[0] + ''.join( [i.capitalize() for i in key.split("_")[1:]]) - if isinstance(d[key], list): - ret[new_key] = [convert_to_camelcase( - i, exclude_keys=exclude_keys) for i in d[key]] - elif isinstance(d[key], dict): - ret[new_key] = convert_to_camelcase(d[key], - exclude_keys=exclude_keys) else: ret[new_key] = d[key] return ret diff --git a/setup.py b/setup.py index 030f831..8282943 100644 --- a/setup.py +++ b/setup.py @@ -26,8 +26,8 @@ 'console_scripts': [ 'pymongoexplain=pymongoexplain.cli_explain:cli_explain'], }, - tests_require=["pymongo==3.10.1"], - install_requires=['pymongo==3.10.1'], + tests_require=["pymongo>=3.10"], + install_requires=['pymongo>=3.10'], python_requires='>=3.5', license="Apache License, Version 2.0", classifiers=[ diff --git a/test/__init__.py b/test/__init__.py index eb417f8..1e756ed 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -11,3 +11,872 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +"""Test suite for pymongo, bson, and gridfs. +""" + +import gc +import os +import socket +import sys +import threading +import time +import unittest +import warnings + +try: + from xmlrunner import XMLTestRunner + HAVE_XML = True +# ValueError is raised when version 3+ is installed on Jython 2.7. +except (ImportError, ValueError): + HAVE_XML = False + +try: + import ipaddress + HAVE_IPADDRESS = True +except ImportError: + HAVE_IPADDRESS = False + +from contextlib import contextmanager +from functools import wraps +from unittest import SkipTest + +import pymongo +import pymongo.errors + +from bson.son import SON +from pymongo import common, message +from pymongo.common import partition_node +from pymongo.ssl_support import HAVE_SSL, validate_cert_reqs +from test.version import Version + +if HAVE_SSL: + import ssl + +try: + # Enable the fault handler to dump the traceback of each running thread + # after a segfault. + import faulthandler + faulthandler.enable() +except ImportError: + pass + +# Enable debug output for uncollectable objects. PyPy does not have set_debug. +if hasattr(gc, 'set_debug'): + gc.set_debug( + gc.DEBUG_UNCOLLECTABLE | + getattr(gc, 'DEBUG_OBJECTS', 0) | + getattr(gc, 'DEBUG_INSTANCES', 0)) + +# The host and port of a single mongod or mongos, or the seed host +# for a replica set. +host = os.environ.get("DB_IP", 'localhost') +port = int(os.environ.get("DB_PORT", 27017)) + +db_user = os.environ.get("DB_USER", "user") +db_pwd = os.environ.get("DB_PASSWORD", "password") + +CERT_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'certificates') +CLIENT_PEM = os.environ.get('CLIENT_PEM', + os.path.join(CERT_PATH, 'client.pem')) +CA_PEM = os.environ.get('CA_PEM', os.path.join(CERT_PATH, 'ca.pem')) + +TLS_OPTIONS = dict(tls=True) +if CLIENT_PEM: + TLS_OPTIONS['tlsCertificateKeyFile'] = CLIENT_PEM +if CA_PEM: + TLS_OPTIONS['tlsCAFile'] = CA_PEM + +COMPRESSORS = os.environ.get("COMPRESSORS") + +def is_server_resolvable(): + """Returns True if 'server' is resolvable.""" + socket_timeout = socket.getdefaulttimeout() + socket.setdefaulttimeout(1) + try: + try: + socket.gethostbyname('server') + return True + except socket.error: + return False + finally: + socket.setdefaulttimeout(socket_timeout) + + +def _create_user(authdb, user, pwd=None, roles=None, **kwargs): + cmd = SON([('createUser', user)]) + # X509 doesn't use a password + if pwd: + cmd['pwd'] = pwd + cmd['roles'] = roles or ['root'] + cmd.update(**kwargs) + return authdb.command(cmd) + + +class client_knobs(object): + def __init__( + self, + heartbeat_frequency=None, + min_heartbeat_interval=None, + kill_cursor_frequency=None, + events_queue_frequency=None): + self.heartbeat_frequency = heartbeat_frequency + self.min_heartbeat_interval = min_heartbeat_interval + self.kill_cursor_frequency = kill_cursor_frequency + self.events_queue_frequency = events_queue_frequency + + self.old_heartbeat_frequency = None + self.old_min_heartbeat_interval = None + self.old_kill_cursor_frequency = None + self.old_events_queue_frequency = None + + def enable(self): + self.old_heartbeat_frequency = common.HEARTBEAT_FREQUENCY + self.old_min_heartbeat_interval = common.MIN_HEARTBEAT_INTERVAL + self.old_kill_cursor_frequency = common.KILL_CURSOR_FREQUENCY + self.old_events_queue_frequency = common.EVENTS_QUEUE_FREQUENCY + + if self.heartbeat_frequency is not None: + common.HEARTBEAT_FREQUENCY = self.heartbeat_frequency + + if self.min_heartbeat_interval is not None: + common.MIN_HEARTBEAT_INTERVAL = self.min_heartbeat_interval + + if self.kill_cursor_frequency is not None: + common.KILL_CURSOR_FREQUENCY = self.kill_cursor_frequency + + if self.events_queue_frequency is not None: + common.EVENTS_QUEUE_FREQUENCY = self.events_queue_frequency + + def __enter__(self): + self.enable() + + def disable(self): + common.HEARTBEAT_FREQUENCY = self.old_heartbeat_frequency + common.MIN_HEARTBEAT_INTERVAL = self.old_min_heartbeat_interval + common.KILL_CURSOR_FREQUENCY = self.old_kill_cursor_frequency + common.EVENTS_QUEUE_FREQUENCY = self.old_events_queue_frequency + + def __exit__(self, exc_type, exc_val, exc_tb): + self.disable() + + +def _all_users(db): + return set(u['user'] for u in db.command('usersInfo').get('users', [])) + + +class ClientContext(object): + + def __init__(self): + """Create a client and grab essential information from the server.""" + self.connection_attempts = [] + self.connected = False + self.w = None + self.nodes = set() + self.replica_set_name = None + self.cmd_line = None + self.server_status = None + self.version = Version(-1) # Needs to be comparable with Version + self.auth_enabled = False + self.test_commands_enabled = False + self.is_mongos = False + self.mongoses = [] + self.is_rs = False + self.has_ipv6 = False + self.tls = False + self.ssl_certfile = False + self.server_is_resolvable = is_server_resolvable() + self.default_client_options = {} + self.sessions_enabled = False + self.client = None + self.conn_lock = threading.Lock() + + if COMPRESSORS: + self.default_client_options["compressors"] = COMPRESSORS + + @property + def ismaster(self): + return self.client.admin.command('isMaster') + + def _connect(self, host, port, **kwargs): + # Jython takes a long time to connect. + if sys.platform.startswith('java'): + timeout_ms = 10000 + else: + timeout_ms = 5000 + if COMPRESSORS: + kwargs["compressors"] = COMPRESSORS + client = pymongo.MongoClient( + host, port, serverSelectionTimeoutMS=timeout_ms, **kwargs) + try: + try: + client.admin.command('isMaster') # Can we connect? + except pymongo.errors.OperationFailure as exc: + # SERVER-32063 + self.connection_attempts.append( + 'connected client %r, but isMaster failed: %s' % ( + client, exc)) + else: + self.connection_attempts.append( + 'successfully connected client %r' % (client,)) + # If connected, then return client with default timeout + return pymongo.MongoClient(host, port, **kwargs) + except pymongo.errors.ConnectionFailure as exc: + self.connection_attempts.append( + 'failed to connect client %r: %s' % (client, exc)) + return None + + def _init_client(self): + self.client = self._connect(host, port) + if HAVE_SSL and not self.client: + # Is MongoDB configured for SSL? + self.client = self._connect(host, port, **TLS_OPTIONS) + if self.client: + self.tls = True + self.default_client_options.update(TLS_OPTIONS) + self.ssl_certfile = True + + if self.client: + self.connected = True + + try: + self.cmd_line = self.client.admin.command('getCmdLineOpts') + except pymongo.errors.OperationFailure as e: + msg = e.details.get('errmsg', '') + if e.code == 13 or 'unauthorized' in msg or 'login' in msg: + # Unauthorized. + self.auth_enabled = True + else: + raise + else: + self.auth_enabled = self._server_started_with_auth() + + if self.auth_enabled: + # See if db_user already exists. + if not self._check_user_provided(): + _create_user(self.client.admin, db_user, db_pwd) + + self.client = self._connect( + host, port, username=db_user, password=db_pwd, + replicaSet=self.replica_set_name, + **self.default_client_options) + + # May not have this if OperationFailure was raised earlier. + self.cmd_line = self.client.admin.command('getCmdLineOpts') + + self.server_status = self.client.admin.command('serverStatus') + if self.storage_engine == "mmapv1": + # MMAPv1 does not support retryWrites=True. + self.default_client_options['retryWrites'] = False + + ismaster = self.ismaster + self.sessions_enabled = 'logicalSessionTimeoutMinutes' in ismaster + + if 'setName' in ismaster: + self.replica_set_name = str(ismaster['setName']) + self.is_rs = True + if self.auth_enabled: + # It doesn't matter which member we use as the seed here. + self.client = pymongo.MongoClient( + host, + port, + username=db_user, + password=db_pwd, + replicaSet=self.replica_set_name, + **self.default_client_options) + else: + self.client = pymongo.MongoClient( + host, + port, + replicaSet=self.replica_set_name, + **self.default_client_options) + + # Get the authoritative ismaster result from the primary. + ismaster = self.ismaster + nodes = [partition_node(node.lower()) + for node in ismaster.get('hosts', [])] + nodes.extend([partition_node(node.lower()) + for node in ismaster.get('passives', [])]) + nodes.extend([partition_node(node.lower()) + for node in ismaster.get('arbiters', [])]) + self.nodes = set(nodes) + else: + self.nodes = set([(host, port)]) + self.w = len(ismaster.get("hosts", [])) or 1 + self.version = Version.from_client(self.client) + + if 'enableTestCommands=1' in self.cmd_line['argv']: + self.test_commands_enabled = True + elif 'parsed' in self.cmd_line: + params = self.cmd_line['parsed'].get('setParameter', []) + if 'enableTestCommands=1' in params: + self.test_commands_enabled = True + else: + params = self.cmd_line['parsed'].get('setParameter', {}) + if params.get('enableTestCommands') == '1': + self.test_commands_enabled = True + + self.is_mongos = (self.ismaster.get('msg') == 'isdbgrid') + self.has_ipv6 = self._server_started_with_ipv6() + if self.is_mongos: + # Check for another mongos on the next port. + address = self.client.address + next_address = address[0], address[1] + 1 + self.mongoses.append(address) + mongos_client = self._connect(*next_address, + **self.default_client_options) + if mongos_client: + ismaster = mongos_client.admin.command('ismaster') + if ismaster.get('msg') == 'isdbgrid': + self.mongoses.append(next_address) + + def init(self): + with self.conn_lock: + if not self.client and not self.connection_attempts: + self._init_client() + + def connection_attempt_info(self): + return '\n'.join(self.connection_attempts) + + @property + def host(self): + if self.is_rs: + primary = self.client.primary + return str(primary[0]) if primary is not None else host + return host + + @property + def port(self): + if self.is_rs: + primary = self.client.primary + return primary[1] if primary is not None else port + return port + + @property + def pair(self): + return "%s:%d" % (self.host, self.port) + + @property + def has_secondaries(self): + if not self.client: + return False + return bool(len(self.client.secondaries)) + + @property + def storage_engine(self): + try: + return self.server_status.get("storageEngine", {}).get("name") + except AttributeError: + # Raised if self.server_status is None. + return None + + def _check_user_provided(self): + """Return True if db_user/db_password is already an admin user.""" + client = pymongo.MongoClient( + host, port, + username=db_user, + password=db_pwd, + serverSelectionTimeoutMS=100, + **self.default_client_options) + + try: + return db_user in _all_users(client.admin) + except pymongo.errors.OperationFailure as e: + msg = e.details.get('errmsg', '') + if e.code == 18 or 'auth fails' in msg: + # Auth failed. + return False + else: + raise + + def _server_started_with_auth(self): + # MongoDB >= 2.0 + if 'parsed' in self.cmd_line: + parsed = self.cmd_line['parsed'] + # MongoDB >= 2.6 + if 'security' in parsed: + security = parsed['security'] + # >= rc3 + if 'authorization' in security: + return security['authorization'] == 'enabled' + # < rc3 + return (security.get('auth', False) or + bool(security.get('keyFile'))) + return parsed.get('auth', False) or bool(parsed.get('keyFile')) + # Legacy + argv = self.cmd_line['argv'] + return '--auth' in argv or '--keyFile' in argv + + def _server_started_with_ipv6(self): + if not socket.has_ipv6: + return False + + if 'parsed' in self.cmd_line: + if not self.cmd_line['parsed'].get('net', {}).get('ipv6'): + return False + else: + if '--ipv6' not in self.cmd_line['argv']: + return False + + # The server was started with --ipv6. Is there an IPv6 route to it? + try: + for info in socket.getaddrinfo(self.host, self.port): + if info[0] == socket.AF_INET6: + return True + except socket.error: + pass + + return False + + def _require(self, condition, msg, func=None): + def make_wrapper(f): + @wraps(f) + def wrap(*args, **kwargs): + self.init() + # Always raise SkipTest if we can't connect to MongoDB + if not self.connected: + raise SkipTest( + "Cannot connect to MongoDB on %s" % (self.pair,)) + if condition(): + return f(*args, **kwargs) + raise SkipTest(msg) + return wrap + + if func is None: + def decorate(f): + return make_wrapper(f) + return decorate + return make_wrapper(func) + + def create_user(self, dbname, user, pwd=None, roles=None, **kwargs): + kwargs['writeConcern'] = {'w': self.w} + return _create_user(self.client[dbname], user, pwd, roles, **kwargs) + + def drop_user(self, dbname, user): + self.client[dbname].command( + 'dropUser', user, writeConcern={'w': self.w}) + + def require_connection(self, func): + """Run a test only if we can connect to MongoDB.""" + return self._require( + lambda: True, # _require checks if we're connected + "Cannot connect to MongoDB on %s" % (self.pair,), + func=func) + + def require_no_mmap(self, func): + """Run a test only if the server is not using the MMAPv1 storage + engine. Only works for standalone and replica sets; tests are + run regardless of storage engine on sharded clusters. """ + def is_not_mmap(): + if self.is_mongos: + return True + return self.storage_engine != 'mmapv1' + + return self._require( + is_not_mmap, "Storage engine must not be MMAPv1", func=func) + + def require_version_min(self, *ver): + """Run a test only if the server version is at least ``version``.""" + other_version = Version(*ver) + return self._require(lambda: self.version >= other_version, + "Server version must be at least %s" + % str(other_version)) + + def require_version_max(self, *ver): + """Run a test only if the server version is at most ``version``.""" + other_version = Version(*ver) + return self._require(lambda: self.version <= other_version, + "Server version must be at most %s" + % str(other_version)) + + def require_auth(self, func): + """Run a test only if the server is running with auth enabled.""" + return self.check_auth_with_sharding( + self._require(lambda: self.auth_enabled, + "Authentication is not enabled on the server", + func=func)) + + def require_no_auth(self, func): + """Run a test only if the server is running without auth enabled.""" + return self._require(lambda: not self.auth_enabled, + "Authentication must not be enabled on the server", + func=func) + + def require_replica_set(self, func): + """Run a test only if the client is connected to a replica set.""" + return self._require(lambda: self.is_rs, + "Not connected to a replica set", + func=func) + + def require_secondaries_count(self, count): + """Run a test only if the client is connected to a replica set that has + `count` secondaries. + """ + def sec_count(): + return 0 if not self.client else len(self.client.secondaries) + return self._require(lambda: sec_count() >= count, + "Not enough secondaries available") + + def require_no_replica_set(self, func): + """Run a test if the client is *not* connected to a replica set.""" + return self._require( + lambda: not self.is_rs, + "Connected to a replica set, not a standalone mongod", + func=func) + + def require_ipv6(self, func): + """Run a test only if the client can connect to a server via IPv6.""" + return self._require(lambda: self.has_ipv6, + "No IPv6", + func=func) + + def require_no_mongos(self, func): + """Run a test only if the client is not connected to a mongos.""" + return self._require(lambda: not self.is_mongos, + "Must be connected to a mongod, not a mongos", + func=func) + + def require_mongos(self, func): + """Run a test only if the client is connected to a mongos.""" + return self._require(lambda: self.is_mongos, + "Must be connected to a mongos", + func=func) + + def require_multiple_mongoses(self, func): + """Run a test only if the client is connected to a sharded cluster + that has 2 mongos nodes.""" + return self._require(lambda: len(self.mongoses) > 1, + "Must have multiple mongoses available", + func=func) + + def require_standalone(self, func): + """Run a test only if the client is connected to a standalone.""" + return self._require(lambda: not (self.is_mongos or self.is_rs), + "Must be connected to a standalone", + func=func) + + def require_no_standalone(self, func): + """Run a test only if the client is not connected to a standalone.""" + return self._require(lambda: self.is_mongos or self.is_rs, + "Must be connected to a replica set or mongos", + func=func) + + def check_auth_with_sharding(self, func): + """Skip a test when connected to mongos < 2.0 and running with auth.""" + condition = lambda: not (self.auth_enabled and + self.is_mongos and self.version < (2,)) + return self._require(condition, + "Auth with sharding requires MongoDB >= 2.0.0", + func=func) + + def is_topology_type(self, topologies): + if 'single' in topologies and not (self.is_mongos or self.is_rs): + return True + if 'replicaset' in topologies and self.is_rs: + return True + if 'sharded' in topologies and self.is_mongos: + return True + return False + + def require_cluster_type(self, topologies=[]): + """Run a test only if the client is connected to a cluster that + conforms to one of the specified topologies. Acceptable topologies + are 'single', 'replicaset', and 'sharded'.""" + def _is_valid_topology(): + return self.is_topology_type(topologies) + return self._require( + _is_valid_topology, + "Cluster type not in %s" % (topologies)) + + def require_test_commands(self, func): + """Run a test only if the server has test commands enabled.""" + return self._require(lambda: self.test_commands_enabled, + "Test commands must be enabled", + func=func) + + def require_failCommand_fail_point(self, func): + """Run a test only if the server supports the failCommand fail + point.""" + return self._require(lambda: self.supports_failCommand_fail_point, + "failCommand fail point must be supported", + func=func) + + def require_tls(self, func): + """Run a test only if the client can connect over TLS.""" + return self._require(lambda: self.tls, + "Must be able to connect via TLS", + func=func) + + def require_no_tls(self, func): + """Run a test only if the client can connect over TLS.""" + return self._require(lambda: not self.tls, + "Must be able to connect without TLS", + func=func) + + def require_ssl_certfile(self, func): + """Run a test only if the client can connect with ssl_certfile.""" + return self._require(lambda: self.ssl_certfile, + "Must be able to connect with ssl_certfile", + func=func) + + def require_server_resolvable(self, func): + """Run a test only if the hostname 'server' is resolvable.""" + return self._require(lambda: self.server_is_resolvable, + "No hosts entry for 'server'. Cannot validate " + "hostname in the certificate", + func=func) + + def require_sessions(self, func): + """Run a test only if the deployment supports sessions.""" + return self._require(lambda: self.sessions_enabled, + "Sessions not supported", + func=func) + + def supports_transactions(self): + if self.storage_engine == 'mmapv1': + return False + + if self.version.at_least(4, 1, 8): + return self.is_mongos or self.is_rs + + if self.version.at_least(4, 0): + return self.is_rs + + return False + + def require_transactions(self, func): + """Run a test only if the deployment might support transactions. + + *Might* because this does not test the storage engine or FCV. + """ + return self._require(self.supports_transactions, + "Transactions are not supported", + func=func) + + def mongos_seeds(self): + return ','.join('%s:%s' % address for address in self.mongoses) + + @property + def supports_reindex(self): + """Does the connected server support reindex?""" + return not ((self.version.at_least(4, 1, 0) and self.is_mongos) or + (self.version.at_least(4, 5, 0) and ( + self.is_mongos or self.is_rs))) + + @property + def supports_getpreverror(self): + """Does the connected server support getpreverror?""" + return not (self.version.at_least(4, 1, 0) or self.is_mongos) + + @property + def supports_failCommand_fail_point(self): + """Does the server support the failCommand fail point?""" + if self.is_mongos: + return (self.version.at_least(4, 1, 5) and + self.test_commands_enabled) + else: + return (self.version.at_least(4, 0) and + self.test_commands_enabled) + + + @property + def requires_hint_with_min_max_queries(self): + """Does the server require a hint with min/max queries.""" + # Changed in SERVER-39567. + return self.version.at_least(4, 1, 10) + + +# Reusable client context +client_context = ClientContext() + + +def sanitize_cmd(cmd): + cp = cmd.copy() + cp.pop('$clusterTime', None) + cp.pop('$db', None) + cp.pop('$readPreference', None) + cp.pop('lsid', None) + # OP_MSG encoding may move the payload type one field to the + # end of the command. Do the same here. + name = next(iter(cp)) + try: + identifier = message._FIELD_MAP[name] + docs = cp.pop(identifier) + cp[identifier] = docs + except KeyError: + pass + return cp + + +def sanitize_reply(reply): + cp = reply.copy() + cp.pop('$clusterTime', None) + cp.pop('operationTime', None) + return cp + + +class PyMongoTestCase(unittest.TestCase): + def assertEqualCommand(self, expected, actual, msg=None): + self.assertEqual(sanitize_cmd(expected), sanitize_cmd(actual), msg) + + def assertEqualReply(self, expected, actual, msg=None): + self.assertEqual(sanitize_reply(expected), sanitize_reply(actual), msg) + + @contextmanager + def fail_point(self, command_args): + cmd_on = SON([('configureFailPoint', 'failCommand')]) + cmd_on.update(command_args) + client_context.client.admin.command(cmd_on) + try: + yield + finally: + client_context.client.admin.command( + 'configureFailPoint', cmd_on['configureFailPoint'], mode='off') + + +class IntegrationTest(PyMongoTestCase): + """Base class for TestCases that need a connection to MongoDB to pass.""" + + @classmethod + @client_context.require_connection + def setUpClass(cls): + cls.client = client_context.client + cls.db = cls.client.pymongo_test + if client_context.auth_enabled: + cls.credentials = {'username': db_user, 'password': db_pwd} + else: + cls.credentials = {} + + +# Use assertRaisesRegex if available, otherwise use Python 2.7's +# deprecated assertRaisesRegexp, with a 'p'. +if not hasattr(unittest.TestCase, 'assertRaisesRegex'): + unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp + + +class MockClientTest(unittest.TestCase): + """Base class for TestCases that use MockClient. + + This class is *not* an IntegrationTest: if properly written, MockClient + tests do not require a running server. + + The class temporarily overrides HEARTBEAT_FREQUENCY to speed up tests. + """ + + def setUp(self): + super(MockClientTest, self).setUp() + + self.client_knobs = client_knobs( + heartbeat_frequency=0.001, + min_heartbeat_interval=0.001) + + self.client_knobs.enable() + + def tearDown(self): + self.client_knobs.disable() + super(MockClientTest, self).tearDown() + + +def setup(): + client_context.init() + warnings.resetwarnings() + warnings.simplefilter("always") + + +def _get_executors(topology): + executors = [] + for server in topology._servers.values(): + # Some MockMonitor do not have an _executor. + executors.append(getattr(server._monitor, '_executor', None)) + executors.append(topology._Topology__events_executor) + if topology._srv_monitor: + executors.append(topology._srv_monitor._executor) + return [e for e in executors if e is not None] + + +def all_executors_stopped(topology): + running = [e for e in _get_executors(topology) if not e._stopped] + if running: + print(' Topology %s has THREADS RUNNING: %s, created at: %s' % ( + topology, running, topology._settings._stack)) + return False + return True + + +def print_unclosed_clients(): + from pymongo.topology import Topology + processed = set() + # Call collect to manually cleanup any would-be gc'd clients to avoid + # false positives. + gc.collect() + for obj in gc.get_objects(): + try: + if isinstance(obj, Topology): + # Avoid printing the same Topology multiple times. + if obj._topology_id in processed: + continue + all_executors_stopped(obj) + processed.add(obj._topology_id) + except ReferenceError: + pass + + +def teardown(): + garbage = [] + for g in gc.garbage: + garbage.append('GARBAGE: %r' % (g,)) + garbage.append(' gc.get_referents: %r' % (gc.get_referents(g),)) + garbage.append(' gc.get_referrers: %r' % (gc.get_referrers(g),)) + if garbage: + assert False, '\n'.join(garbage) + c = client_context.client + if c: + c.drop_database("pymongo-pooling-tests") + c.drop_database("pymongo_test") + c.drop_database("pymongo_test1") + c.drop_database("pymongo_test2") + c.drop_database("pymongo_test_mike") + c.drop_database("pymongo_test_bernie") + c.close() + + # Jython does not support gc.get_objects. + if not sys.platform.startswith('java'): + print_unclosed_clients() + + +class PymongoTestRunner(unittest.TextTestRunner): + def run(self, test): + setup() + result = super(PymongoTestRunner, self).run(test) + teardown() + return result + + +if HAVE_XML: + class PymongoXMLTestRunner(XMLTestRunner): + def run(self, test): + setup() + result = super(PymongoXMLTestRunner, self).run(test) + teardown() + return result + + +def test_cases(suite): + """Iterator over all TestCases within a TestSuite.""" + for suite_or_case in suite._tests: + if isinstance(suite_or_case, unittest.TestCase): + # unittest.TestCase + yield suite_or_case + else: + # unittest.TestSuite + for case in test_cases(suite_or_case): + yield case + + +# Helper method to workaround https://bugs.python.org/issue21724 +def clear_warning_registry(): + """Clear the __warningregistry__ for all modules.""" + for name, module in list(sys.modules.items()): + if hasattr(module, "__warningregistry__"): + setattr(module, "__warningregistry__", {}) diff --git a/test/certificates/ca.pem b/test/certificates/ca.pem new file mode 100644 index 0000000..6ac86cf --- /dev/null +++ b/test/certificates/ca.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfzCCAmegAwIBAgIDB1MGMA0GCSqGSIb3DQEBCwUAMHkxGzAZBgNVBAMTEkRy +aXZlcnMgVGVzdGluZyBDQTEQMA4GA1UECxMHRHJpdmVyczEQMA4GA1UEChMHTW9u +Z29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlvcmsx +CzAJBgNVBAYTAlVTMB4XDTE5MDUyMjIwMjMxMVoXDTM5MDUyMjIwMjMxMVoweTEb +MBkGA1UEAxMSRHJpdmVycyBUZXN0aW5nIENBMRAwDgYDVQQLEwdEcml2ZXJzMRAw +DgYDVQQKEwdNb25nb0RCMRYwFAYDVQQHEw1OZXcgWW9yayBDaXR5MREwDwYDVQQI +EwhOZXcgWW9yazELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCl7VN+WsQfHlwapcOpTLZVoeMAl1LTbWTFuXSAavIyy0W1Ytky1UP/ +bxCSW0mSWwCgqoJ5aXbAvrNRp6ArWu3LsTQIEcD3pEdrFIVQhYzWUs9fXqPyI9k+ +QNNQ+MRFKeGteTPYwF2eVEtPzUHU5ws3+OKp1m6MCLkwAG3RBFUAfddUnLvGoZiT +pd8/eNabhgHvdrCw+tYFCWvSjz7SluEVievpQehrSEPKe8DxJq/IM3tSl3tdylzT +zeiKNO7c7LuQrgjAfrZl7n2SriHIlNmqiDR/kdd8+TxBuxjFlcf2WyHCO3lIcIgH +KXTlhUCg50KfHaxHu05Qw0x8869yIzqbAgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAEHuhTL8KQZcKCTSJbYA9MgZj7U32arMGBbc1hiq +VBREwvdVz4+9tIyWMzN9R/YCKmUTnCq8z3wTlC8kBtxYn/l4Tj8nJYcgLJjQ0Fwe +gT564CmvkUat8uXPz6olOCdwkMpJ9Sj62i0mpgXJdBfxKQ6TZ9yGz6m3jannjZpN +LchB7xSAEWtqUgvNusq0dApJsf4n7jZ+oBZVaQw2+tzaMfaLqHgMwcu1FzA8UKCD +sxCgIsZUs8DdxaD418Ot6nPfheOTqe24n+TTa+Z6O0W0QtnofJBx7tmAo1aEc57i +77s89pfwIJetpIlhzNSMKurCAocFCJMJLAASJFuu6dyDvPo= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/certificates/client.pem b/test/certificates/client.pem new file mode 100644 index 0000000..5b07001 --- /dev/null +++ b/test/certificates/client.pem @@ -0,0 +1,48 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAsNS8UEuin7/K29jXfIOLpIoh1jEyWVqxiie2Onx7uJJKcoKo +khA3XeUnVN0k6X5MwYWcN52xcns7LYtyt06nRpTG2/emoV44w9uKTuHsvUbiOwSV +m/ToKQQ4FUFZoqorXH+ZmJuIpJNfoW+3CkE1vEDCIecIq6BNg5ySsPtvSuSJHGjp +mc7/5ZUDvFE2aJ8QbJU3Ws0HXiEb6ymi048LlzEL2VKX3w6mqqh+7dcZGAy7qYk2 +5FZ9ktKvCeQau7mTyU1hsPrKFiKtMN8Q2ZAItX13asw5/IeSTq2LgLFHlbj5Kpq4 +GmLdNCshzH5X7Ew3IYM8EHmsX8dmD6mhv7vpVwIDAQABAoIBABOdpb4qhcG+3twA +c/cGCKmaASLnljQ/UU6IFTjrsjXJVKTbRaPeVKX/05sgZQXZ0t3s2mV5AsQ2U1w8 +Cd+3w+qaemzQThW8hAOGCROzEDX29QWi/o2sX0ydgTMqaq0Wv3SlWv6I0mGfT45y +/BURIsrdTCvCmz2erLqa1dL4MWJXRFjT9UTs5twlecIOM2IHKoGGagFhymRK4kDe +wTRC9fpfoAgyfus3pCO/wi/F8yKGPDEwY+zgkhrJQ+kSeki7oKdGD1H540vB8gRt +EIqssE0Y6rEYf97WssQlxJgvoJBDSftOijS6mwvoasDUwfFqyyPiirawXWWhHXkc +DjIi/XECgYEA5xfjilw9YyM2UGQNESbNNunPcj7gDZbN347xJwmYmi9AUdPLt9xN +3XaMqqR22k1DUOxC/5hH0uiXir7mDfqmC+XS/ic/VOsa3CDWejkEnyGLiwSHY502 +wD/xWgHwUiGVAG9HY64vnDGm6L3KGXA2oqxanL4V0+0+Ht49pZ16i8sCgYEAw+Ox +CHGtpkzjCP/z8xr+1VTSdpc/4CP2HONnYopcn48KfQnf7Nale69/1kZpypJlvQSG +eeA3jMGigNJEkb8/kaVoRLCisXcwLc0XIfCTeiK6FS0Ka30D/84Qm8UsHxRdpGkM +kYITAa2r64tgRL8as4/ukeXBKE+oOhX43LeEfyUCgYBkf7IX2Ndlhsm3GlvIarxy +NipeP9PGdR/hKlPbq0OvQf9R1q7QrcE7H7Q6/b0mYNV2mtjkOQB7S2WkFDMOP0P5 +BqDEoKLdNkV/F9TOYH+PCNKbyYNrodJOt0Ap6Y/u1+Xpw3sjcXwJDFrO+sKqX2+T +PStG4S+y84jBedsLbDoAEwKBgQCTz7/KC11o2yOFqv09N+WKvBKDgeWlD/2qFr3w +UU9K5viXGVhqshz0k5z25vL09Drowf1nAZVpFMO2SPOMtq8VC6b+Dfr1xmYIaXVH +Gu1tf77CM9Zk/VSDNc66e7GrUgbHBK2DLo+A+Ld9aRIfTcSsMbNnS+LQtCrQibvb +cG7+MQKBgQCY11oMT2dUekoZEyW4no7W5D74lR8ztMjp/fWWTDo/AZGPBY6cZoZF +IICrzYtDT/5BzB0Jh1f4O9ZQkm5+OvlFbmoZoSbMzHL3oJCBOY5K0/kdGXL46WWh +IRJSYakNU6VIS7SjDpKgm9D8befQqZeoSggSjIIULIiAtYgS80vmGA== +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDgzCCAmugAwIBAgIDAxOUMA0GCSqGSIb3DQEBCwUAMHkxGzAZBgNVBAMTEkRy +aXZlcnMgVGVzdGluZyBDQTEQMA4GA1UECxMHRHJpdmVyczEQMA4GA1UEChMHTW9u +Z29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlvcmsx +CzAJBgNVBAYTAlVTMB4XDTE5MDUyMjIzNTU1NFoXDTM5MDUyMjIzNTU1NFowaTEP +MA0GA1UEAxMGY2xpZW50MRAwDgYDVQQLEwdEcml2ZXJzMQwwCgYDVQQKEwNNREIx +FjAUBgNVBAcTDU5ldyBZb3JrIENpdHkxETAPBgNVBAgTCE5ldyBZb3JrMQswCQYD +VQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALDUvFBLop+/ +ytvY13yDi6SKIdYxMllasYontjp8e7iSSnKCqJIQN13lJ1TdJOl+TMGFnDedsXJ7 +Oy2LcrdOp0aUxtv3pqFeOMPbik7h7L1G4jsElZv06CkEOBVBWaKqK1x/mZibiKST +X6FvtwpBNbxAwiHnCKugTYOckrD7b0rkiRxo6ZnO/+WVA7xRNmifEGyVN1rNB14h +G+spotOPC5cxC9lSl98Opqqofu3XGRgMu6mJNuRWfZLSrwnkGru5k8lNYbD6yhYi +rTDfENmQCLV9d2rMOfyHkk6ti4CxR5W4+SqauBpi3TQrIcx+V+xMNyGDPBB5rF/H +Zg+pob+76VcCAwEAAaMkMCIwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUF +BwMCMA0GCSqGSIb3DQEBCwUAA4IBAQAqRcLAGvYMaGYOV4HJTzNotT2qE0I9THNQ +wOV1fBg69x6SrUQTQLjJEptpOA288Wue6Jt3H+p5qAGV5GbXjzN/yjCoItggSKxG +Xg7279nz6/C5faoIKRjpS9R+MsJGlttP9nUzdSxrHvvqm62OuSVFjjETxD39DupE +YPFQoHOxdFTtBQlc/zIKxVdd20rs1xJeeU2/L7jtRBSPuR/Sk8zot7G2/dQHX49y +kHrq8qz12kj1T6XDXf8KZawFywXaz0/Ur+fUYKmkVk1T0JZaNtF4sKqDeNE4zcns +p3xLVDSl1Q5Gwj7bgph9o4Hxs9izPwiqjmNaSjPimGYZ399zcurY +-----END CERTIFICATE----- diff --git a/test/certificates/crl.pem b/test/certificates/crl.pem new file mode 100644 index 0000000..733a0ac --- /dev/null +++ b/test/certificates/crl.pem @@ -0,0 +1,13 @@ +-----BEGIN X509 CRL----- +MIIB6jCB0wIBATANBgkqhkiG9w0BAQsFADB5MRswGQYDVQQDExJEcml2ZXJzIFRl +c3RpbmcgQ0ExEDAOBgNVBAsTB0RyaXZlcnMxEDAOBgNVBAoTB01vbmdvREIxFjAU +BgNVBAcTDU5ldyBZb3JrIENpdHkxETAPBgNVBAgTCE5ldyBZb3JrMQswCQYDVQQG +EwJVUxcNMTkwNTIyMjI0NTUzWhcNMTkwNjIxMjI0NTUzWjAVMBMCAncVFw0xOTA1 +MjIyMjQ1MzJaoA8wDTALBgNVHRQEBAICEAAwDQYJKoZIhvcNAQELBQADggEBACwQ +W9OF6ExJSzzYbpCRroznkfdLG7ghNSxIpBQUGtcnYbkP4em6TdtAj5K3yBjcKn4a +hnUoa5EJGr2Xgg0QascV/1GuWEJC9rsYYB9boVi95l1CrkS0pseaunM086iItZ4a +hRVza8qEMBc3rdsracA7hElYMKdFTRLpIGciJehXzv40yT5XFBHGy/HIT0CD50O7 +BDOHzA+rCFCvxX8UY9myDfb1r1zUW7Gzjn241VT7bcIJmhFE9oV0popzDyqr6GvP +qB2t5VmFpbnSwkuc4ie8Jizip1P8Hg73lut3oVAHACFGPpfaNIAp4GcSH61zJmff +9UBe3CJ1INwqyiuqGeA= +-----END X509 CRL----- diff --git a/test/certificates/password_protected.pem b/test/certificates/password_protected.pem new file mode 100644 index 0000000..cc9e124 --- /dev/null +++ b/test/certificates/password_protected.pem @@ -0,0 +1,51 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIC8as6PDVhwECAggA +MB0GCWCGSAFlAwQBAgQQTYOgCJcRqUI7dsgqNojv/ASCBNCG9fiu642V4AuFK34c +Q42lvy/cR0CIXLq/rDXN1L685kdeKex7AfDuRtnjY2+7CLJiJimgQNJXDJPHab/k +MBHbwbBs38fg6eSYX8V08/IyyTege5EJMhYxmieHDC3DXKt0gyHk6hA/r5+Mr49h +HeVGwqBLJEQ3gVIeHaOleZYspsXXWqOPHnFiqnk/biaJS0+LkDDEiQgTLEYSnOjP +lexxUc4BV/TN0Z920tZCMfwx7IXD/C+0AkV/Iqq4LALmT702EccB3indaIJ8biGR +radqDLR32Q+vT9uZHgT8EFiUsISMqhob2mnyTfFV/s9ghWwogjSz0HrRcq6fxdg7 +oeyT9K0ET53AGTGmV0206byPu6qCj1eNvtn+t1Ob+d5hecaTugRMVheWPlc5frsz +AcewDNa0pv4pZItjAGMqOPJHfzEDnzTJXpLqGYhg044H1+OCY8+1YK7U0u8dO+/3 +f5AoDMq18ipDVTFTooJURej4/Wjbrfad3ZFjp86nxfHPeWM1YjC9+IlLtK1wr0/U +V8TjGqCkw8yHayz01A86iA8X53YQBg+tyMGjxmivo6LgFGKa9mXGvDkN+B+0+OcA +PqldAuH/TJhnkqzja767e4n9kcr+TmV19Hn1hcJPTDrRU8+sSqQFsWN4pvHazAYB +UdWie+EXI0eU2Av9JFgrVcpRipXjB48BaPwuBw8hm+VStCH7ynF4lJy6/3esjYwk +Mx+NUf8+pp1DRzpzuJa2vAutzqia5r58+zloQMxkgTZtJkQU6OCRoUhHGVk7WNb1 +nxsibOSzyVSP9ZNbHIHAn43vICFGrPubRs200Kc4CdXsOSEWoP0XYebhiNJgGtQs +KoISsV4dFRLwhaJhIlayTBQz6w6Ph87WbtuiAqoLiuqdXhUGz/79j/6JZqCH8t/H +eZs4Dhu+HdD/wZKJDYAS+JBsiwYWnI3y/EowZYgLdOMI4u6xYDejhxwEw20LW445 +qjJ7pV/iX2uavazHgC91Bfd4zodfXIQ1IDyTmb51UFwx0ARzG6enntduO6xtcYU9 +MXwfrEpuZ/MkWTLkR0PHPbIPcR1MiVwPKdvrLk42Bzj/urtXYrAFUckMFMzEh+uv +0lix2hbq/Xwj4dXcY4w9hnC6QQDCJTf9S6MU6OisrZHKk0qZ2Vb4aU/eBcBsHBwo +X/QGcDHneHxlrrs2eLX26Vh8Odc5h8haeIxnfaa1t+Yv56OKHuAztPMnJOUL7KtQ +A556LxT0b5IGx0RcfUcbG8XbxEHseACptoDOoguh9923IBI0uXmpi8q0P815LPUu +0AsE47ATDMGPnXbopejRDicfgMGjykJn8vKO8r/Ia3Fpnomx4iJNCXGqomL+GMpZ +IhQbKNrRG6XZMlx5kVCT0Qr1nOWMiOTSDCQ5vrG3c1Viu+0bctvidEvs+LCm98tb +7ty8F0uOno0rYGNQz18OEE1Tj+E19Vauz1U35Z5SsgJJ/GfzhSJ79Srmdg2PsAzk +AUNTKXux1GLf1cMjTiiU5g+tCEtUL9Me7lsv3L6aFdrCyRbhXUQfJh4NAG8+3Pvh +EaprThBzKsVvbOfU81mOaH9YMmUgmxG86vxDiNtaWd4v6c1k+HGspJr/q49pcXZP +ltBMuS9AihstZ1sHJsyQCmNXkA== +-----END ENCRYPTED PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDgzCCAmugAwIBAgIDBXUHMA0GCSqGSIb3DQEBCwUAMHkxGzAZBgNVBAMTEkRy +aXZlcnMgVGVzdGluZyBDQTEQMA4GA1UECxMHRHJpdmVyczEQMA4GA1UEChMHTW9u +Z29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlvcmsx +CzAJBgNVBAYTAlVTMB4XDTE5MDUyMzAwMDEyOVoXDTM5MDUyMzAwMDEyOVowaTEP +MA0GA1UEAxMGY2xpZW50MRAwDgYDVQQLEwdEcml2ZXJzMQwwCgYDVQQKEwNNREIx +FjAUBgNVBAcTDU5ldyBZb3JrIENpdHkxETAPBgNVBAgTCE5ldyBZb3JrMQswCQYD +VQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOqCb0Lo4XsV +W327Wlnqc5rwWa5Elw0rFuehSfViRIcYfuFWAPXoOj3fIDsYz6d41G8hp6tkF88p +swlbzDF8Fc7mXDhauwwl2F/NrWYUXwCT8fKju4DtGd2JlDMi1TRDeofkYCGVPp70 +vNqd0H8iDWWs8OmiNrdBLJwNiGaf9y15ena4ImQGitXLFn+qNSXYJ1Rs8p7Y2PTr +L+dff5gJCVbANwGII1rjMAsrMACPVmr8c1Lxoq4fSdJiLweosrv2Lk0WWGsO0Seg +ZY71dNHEyNjItE+VtFEtslJ5L261i3BfF/FqNnH2UmKXzShwfwxyHT8o84gSAltQ +5/lVJ4QQKosCAwEAAaMkMCIwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUF +BwMCMA0GCSqGSIb3DQEBCwUAA4IBAQBOAlKxIMFcTZ+4k8NJv97RSf+zOb5Wu2ct +uxSZxzgKTxLFUuEM8XQiEz1iHQ3XG+uV1fzA74YLQiKjjLrU0mx54eM1vaRtOXvF +sJlzZU8Z2+523FVPx4HBPyObQrfXmIoAiHoQ4VUeepkPRpXxpifgWd/OCWhLDr2/ +0Kgcb0ybaGVDpA0UD9uVIwgFjRu6id7wG+lVcdRxJYskTOOaN2o1hMdAKkrpFQbd +zNRfEoBPUYR3QAmAKP2HBjpgp4ktOHoOKMlfeAuuMCUocSnmPKc3xJaH/6O7rHcf +/Rm0X411RH8JfoXYsSiPsd601kZefhuWvJH0sJLibRDvT7zs8C1v +-----END CERTIFICATE----- diff --git a/test/certificates/server.pem b/test/certificates/server.pem new file mode 100644 index 0000000..e745e03 --- /dev/null +++ b/test/certificates/server.pem @@ -0,0 +1,49 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAhNrB0E6GY/kFSd8/vNpu/t952tbnOsD5drV0XPvmuy7SgKDY +a/S+xb/jPnlZKKehdBnH7qP/gYbv34ZykzcDFZscjPLiGc2cRGP+NQCSFK0d2/7d +y15zSD3zhj14G8+MkpAejTU+0/qFNZMc5neDvGanTe0+8aWa0DXssM0MuTxIv7j6 +CtsMWeqLLofN7a1Kw2UvmieCHfHMuA/08pJwRnV/+5T9WONBPJja2ZQRrG1BjpI4 +81zSPUZesIqi8yDlExdvgNaRZIEHi/njREqwVgJOZomUY57zmKypiMzbz48dDTsV +gUStxrEqbaP+BEjQYPX5+QQk4GdMjkLf52LR6QIDAQABAoIBAHSs+hHLJNOf2zkp +S3y8CUblVMsQeTpsR6otaehPgi9Zy50TpX4KD5D0GMrBH8BIl86y5Zd7h+VlcDzK +gs0vPxI2izhuBovKuzaE6rf5rFFkSBjxGDCG3o/PeJOoYFdsS3RcBbjVzju0hFCs +xnDQ/Wz0anJRrTnjyraY5SnQqx/xuhLXkj/lwWoWjP2bUqDprnuLOj16soNu60Um +JziWbmWx9ty0wohkI/8DPBl9FjSniEEUi9pnZXPElFN6kwPkgdfT5rY/TkMH4lsu +ozOUc5xgwlkT6kVjXHcs3fleuT/mOfVXLPgNms85JKLucfd6KiV7jYZkT/bXIjQ+ +7CZEn0ECgYEA5QiKZgsfJjWvZpt21V/i7dPje2xdwHtZ8F9NjX7ZUFA7mUPxUlwe +GiXxmy6RGzNdnLOto4SF0/7ebuF3koO77oLup5a2etL+y/AnNAufbu4S5D72sbiz +wdLzr3d5JQ12xeaEH6kQNk2SD5/ShctdS6GmTgQPiJIgH0MIdi9F3v0CgYEAlH84 +hMWcC+5b4hHUEexeNkT8kCXwHVcUjGRaYFdSHgovvWllApZDHSWZ+vRcMBdlhNPu +09Btxo99cjOZwGYJyt20QQLGc/ZyiOF4ximQzabTeFgLkTH3Ox6Mh2Rx9yIruYoX +nE3UfMDkYELanEJUv0zenKpZHw7tTt5yXXSlEF0CgYBSsEOvVcKYO/eoluZPYQAA +F2jgzZ4HeUFebDoGpM52lZD+463Dq2hezmYtPaG77U6V3bUJ/TWH9VN/Or290vvN +v83ECcC2FWlSXdD5lFyqYx/E8gqE3YdgqfW62uqM+xBvoKsA9zvYLydVpsEN9v8m +6CSvs/2btA4O21e5u5WBTQKBgGtAb6vFpe0gHRDs24SOeYUs0lWycPhf+qFjobrP +lqnHpa9iPeheat7UV6BfeW3qmBIVl/s4IPE2ld4z0qqZiB0Tf6ssu/TpXNPsNXS6 +dLFz+myC+ufFdNEoQUtQitd5wKbjTCZCOGRaVRgJcSdG6Tq55Fa22mOKPm+mTmed +ZdKpAoGAFsTYBAHPxs8nzkCJCl7KLa4/zgbgywO6EcQgA7tfelB8bc8vcAMG5o+8 +YqAfwxrzhVSVbJx0fibTARXROmbh2pn010l2wj3+qUajM8NiskCPFbSjGy7HSUze +P8Kt1uMDJdj55gATzn44au31QBioZY2zXleorxF21cr+BZCJgfA= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDlTCCAn2gAwIBAgICdxUwDQYJKoZIhvcNAQELBQAweTEbMBkGA1UEAxMSRHJp +dmVycyBUZXN0aW5nIENBMRAwDgYDVQQLEwdEcml2ZXJzMRAwDgYDVQQKEwdNb25n +b0RCMRYwFAYDVQQHEw1OZXcgWW9yayBDaXR5MREwDwYDVQQIEwhOZXcgWW9yazEL +MAkGA1UEBhMCVVMwHhcNMTkwNTIyMjIzMjU2WhcNMzkwNTIyMjIzMjU2WjBwMRIw +EAYDVQQDEwlsb2NhbGhvc3QxEDAOBgNVBAsTB0RyaXZlcnMxEDAOBgNVBAoTB01v +bmdvREIxFjAUBgNVBAcTDU5ldyBZb3JrIENpdHkxETAPBgNVBAgTCE5ldyBZb3Jr +MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAITa +wdBOhmP5BUnfP7zabv7fedrW5zrA+Xa1dFz75rsu0oCg2Gv0vsW/4z55WSinoXQZ +x+6j/4GG79+GcpM3AxWbHIzy4hnNnERj/jUAkhStHdv+3ctec0g984Y9eBvPjJKQ +Ho01PtP6hTWTHOZ3g7xmp03tPvGlmtA17LDNDLk8SL+4+grbDFnqiy6Hze2tSsNl +L5ongh3xzLgP9PKScEZ1f/uU/VjjQTyY2tmUEaxtQY6SOPNc0j1GXrCKovMg5RMX +b4DWkWSBB4v540RKsFYCTmaJlGOe85isqYjM28+PHQ07FYFErcaxKm2j/gRI0GD1 ++fkEJOBnTI5C3+di0ekCAwEAAaMwMC4wLAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/ +AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQBol8+YH7MA +HwnIh7KcJ8h87GkCWsjOJCDJWiYBJArQ0MmgDO0qdx+QEtvLMn3XNtP05ZfK0WyX +or4cWllAkMFYaFbyB2hYazlD1UAAG+22Rku0UP6pJMLbWe6pnqzx+RL68FYdbZhN +fCW2xiiKsdPoo2VEY7eeZKrNr/0RFE5EKXgzmobpTBQT1Dl3Ve4aWLoTy9INlQ/g +z40qS7oq1PjjPLgxINhf4ncJqfmRXugYTOnyFiVXLZTys5Pb9SMKdToGl3NTYWLL +2AZdjr6bKtT+WtXyHqO0cQ8CkAW0M6VOlMluACllcJxfrtdlQS2S4lUIj76QKBdZ +khBHXq/b8MFX +-----END CERTIFICATE----- diff --git a/test/certificates/trusted-ca.pem b/test/certificates/trusted-ca.pem new file mode 100644 index 0000000..a6f6f31 --- /dev/null +++ b/test/certificates/trusted-ca.pem @@ -0,0 +1,82 @@ +# CA bundle file used to test tlsCAFile loading for OCSP. +# Copied from the server: +# https://github.com/mongodb/mongo/blob/r4.3.4/jstests/libs/trusted-ca.pem + +# Autogenerated file, do not edit. +# Generate using jstests/ssl/x509/mkcert.py --config jstests/ssl/x509/certs.yml trusted-ca.pem +# +# CA for alternate client/server certificate chain. +-----BEGIN CERTIFICATE----- +MIIDojCCAooCBG585gswDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxETAP +BgNVBAgMCE5ldyBZb3JrMRYwFAYDVQQHDA1OZXcgWW9yayBDaXR5MRAwDgYDVQQK +DAdNb25nb0RCMQ8wDQYDVQQLDAZLZXJuZWwxHzAdBgNVBAMMFlRydXN0ZWQgS2Vy +bmVsIFRlc3QgQ0EwHhcNMTkwOTI1MjMyNzQxWhcNMzkwOTI3MjMyNzQxWjB8MQsw +CQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxFjAUBgNVBAcMDU5ldyBZb3Jr +IENpdHkxEDAOBgNVBAoMB01vbmdvREIxDzANBgNVBAsMBktlcm5lbDEfMB0GA1UE +AwwWVHJ1c3RlZCBLZXJuZWwgVGVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANlRxtpMeCGhkotkjHQqgqvO6O6hoRoAGGJlDaTVtqrjmC8nwySz +1nAFndqUHttxS3A5j4enOabvffdOcV7+Z6vDQmREF6QZmQAk81pmazSc3wOnRiRs +AhXjld7i+rhB50CW01oYzQB50rlBFu+ONKYj32nBjD+1YN4AZ2tuRlbxfx2uf8Bo +Zowfr4n9nHVcWXBLFmaQLn+88WFO/wuwYUOn6Di1Bvtkvqum0or5QeAF0qkJxfhg +3a4vBnomPdwEXCgAGLvHlB41CWG09EuAjrnE3HPPi5vII8pjY2dKKMomOEYmA+KJ +AC1NlTWdN0TtsoaKnyhMMhLWs3eTyXL7kbkCAwEAAaMxMC8wDAYDVR0TBAUwAwEB +/zAfBgNVHREEGDAWgglsb2NhbGhvc3SCCTEyNy4wLjAuMTANBgkqhkiG9w0BAQsF +AAOCAQEAQk56MO9xAhtO077COCqIYe6pYv3uzOplqjXpJ7Cph7GXwQqdFWfKls7B +cLfF/fhIUZIu5itStEkY+AIwht4mBr1F5+hZUp9KZOed30/ewoBXAUgobLipJV66 +FKg8NRtmJbiZrrC00BSO+pKfQThU8k0zZjBmNmpjxnbKZZSFWUKtbhHV1vujver6 +SXZC7R6692vLwRBMoZxhgy/FkYRdiN0U9wpluKd63eo/O02Nt6OEMyeiyl+Z3JWi +8g5iHNrBYGBbGSnDOnqV6tjEY3eq600JDWiodpA1OQheLi78pkc/VQZwof9dyBCm +6BoCskTjip/UB+vIhdPFT9sgUdgDTg== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDZUcbaTHghoZKL +ZIx0KoKrzujuoaEaABhiZQ2k1baq45gvJ8Mks9ZwBZ3alB7bcUtwOY+Hpzmm7333 +TnFe/merw0JkRBekGZkAJPNaZms0nN8Dp0YkbAIV45Xe4vq4QedAltNaGM0AedK5 +QRbvjjSmI99pwYw/tWDeAGdrbkZW8X8drn/AaGaMH6+J/Zx1XFlwSxZmkC5/vPFh +Tv8LsGFDp+g4tQb7ZL6rptKK+UHgBdKpCcX4YN2uLwZ6Jj3cBFwoABi7x5QeNQlh +tPRLgI65xNxzz4ubyCPKY2NnSijKJjhGJgPiiQAtTZU1nTdE7bKGip8oTDIS1rN3 +k8ly+5G5AgMBAAECggEAS7GjLKgT88reSzUTgubHquYf1fZwMak01RjTnsVdoboy +aMJVwzPsjgo2yEptUQvuNcGmz54cg5vJaVlmPaspGveg6WGaRmswEo/MP4GK98Fo +IFKkKM2CEHO74O14XLN/w8yFA02+IdtM3X/haEFE71VxXNmwawRXIBxN6Wp4j5Fb +mPLKIspnWQ/Y/Fn799sCFAzX5mKkbCt1IEgKssgQQEm1UkvmCkcZE+mdO/ErYP8A +COO0LpM+TK6WQY2LKiteeCCiosTZFb1GO7MkXrRP5uOBZKaW5kq1R0b6PcopJPCM +OcYF0Zli6KB7oiQLdXgU2jCaxYOnuRb6RYh2l7NvAQKBgQD6CZ9TKOn/EUQtukyw +pvYTyt1hoLXqYGcbRtLc1gcC+Z2BD28hd3eD/mEUv+g/8bq/OP4wYV9X+VRvR8xN +MmfAG/sJeOCOClz1A1TyNeA+G0GZ25qWHyHQ2W4WlSG1CXQgxGzU6wo/t6wiVW5R +O4jplFVEOXznf4vmVfBJK50R2QKBgQDegGxm23jF2N5sIYDZ14oxms8bbjPz8zH6 +tiIRYNGbSzI7J4KFGY2HiBwtf1yxS22HBL69Y1WrEzGm1vm4aZG/GUwBzI79QZAO ++YFIGaIrdlv12Zm6lpJMmAWlOs9XFirC17oQEwOQFweOdQSt7F/+HMZOigdikRBV +pK+8Kfay4QKBgQDarDevHwUmkg8yftA7Xomv3aenjkoK5KzH6jTX9kbDj1L0YG8s +sbLQuVRmNUAFTH+qZUnJPh+IbQIvIHfIu+CI3u+55QFeuCl8DqHoAr5PEr9Ys/qK +eEe2w7HIBj0oe1AYqDEWNUkNWLEuhdCpMowW3CeGN1DJlX7gvyAang4MYQKBgHwM +aWNnFQxo/oiWnTnWm2tQfgszA7AMdF7s0E2UBwhnghfMzU3bkzZuwhbznQATp3rR +QG5iRU7dop7717ni0akTN3cBTu8PcHuIy3UhJXLJyDdnG/gVHnepgew+v340E58R +muB/WUsqK8JWp0c4M8R+0mjTN47ShaLZ8EgdtTbBAoGBAKOcpuDfFEMI+YJgn8zX +h0nFT60LX6Lx+zcSDY9+6J6a4n5NhC+weYCDFOGlsLka1SwHcg1xanfrLVjpH7Ok +HDJGLrSh1FP2Rq/oFxZ/OKCjonHLa8IulqD/AA+sqYRbysKNsT3Pi0554F2xFEqQ +z/C84nlT1R2uTCWIxvrnpU2h +-----END PRIVATE KEY----- +# Pre Oct 2019 trusted-ca.pem +# Transitional pending BUILD update. +-----BEGIN CERTIFICATE----- +MIIDpjCCAo6gAwIBAgIDAghHMA0GCSqGSIb3DQEBBQUAMHwxHzAdBgNVBAMTFlRy +dXN0ZWQgS2VybmVsIFRlc3QgQ0ExDzANBgNVBAsTBktlcm5lbDEQMA4GA1UEChMH +TW9uZ29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlv +cmsxCzAJBgNVBAYTAlVTMB4XDTE2MDMzMTE0NTY1NVoXDTM2MDMzMTE0NTY1NVow +fDEfMB0GA1UEAxMWVHJ1c3RlZCBLZXJuZWwgVGVzdCBDQTEPMA0GA1UECxMGS2Vy +bmVsMRAwDgYDVQQKEwdNb25nb0RCMRYwFAYDVQQHEw1OZXcgWW9yayBDaXR5MREw +DwYDVQQIEwhOZXcgWW9yazELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCePFHZTydC96SlSHSyu73vw//ddaE33kPllBB9DP2L7yRF +6D/blFmno9fSM+Dfg64VfGV+0pCXPIZbpH29nzJu0DkvHzKiWK7P1zUj8rAHaX++ +d6k0yeTLFM9v+7YE9rHoANVn22aOyDvTgAyMmA0CLn+SmUy6WObwMIf9cZn97Znd +lww7IeFNyK8sWtfsVN4yRBnjr7kKN2Qo0QmWeFa7jxVQptMJQrY8k1PcyVUOgOjQ +ocJLbWLlm9k0/OMEQSwQHJ+d9weUbKjlZ9ExOrm4QuuA2tJhb38baTdAYw3Jui4f +yD6iBAGD0Jkpc+3YaWv6CBmK8NEFkYJD/gn+lJ75AgMBAAGjMTAvMAwGA1UdEwQF +MAMBAf8wHwYDVR0RBBgwFoIJbG9jYWxob3N0ggkxMjcuMC4wLjEwDQYJKoZIhvcN +AQEFBQADggEBADYikjB6iwAUs6sglwkE4rOkeMkJdRCNwK/5LpFJTWrDjBvBQCdA +Y5hlAVq8PfIYeh+wEuSvsEHXmx7W29X2+p4VuJ95/xBA6NLapwtzuiijRj2RBAOG +1EGuyFQUPTL27DR3+tfayNykDclsVDNN8+l7nt56j8HojP74P5OMHtn+6HX5+mtF +FfZMTy0mWguCsMOkZvjAskm6s4U5gEC8pYEoC0ZRbfUdyYsxZe/nrXIFguVlVPCB +XnfB/0iG9t+VH5cUVj1LP9skXTW4kXfhQmljUuo+EVBNR6n2nfTnpoC65WeAgHV4 +V+s9mJsUv2x72KtKYypqEVT0gaJ1WIN9N1s= +-----END CERTIFICATE----- diff --git a/test/crud/v2/aggregate-merge.json b/test/crud/v2/aggregate-merge.json new file mode 100644 index 0000000..c61736a --- /dev/null +++ b/test/crud/v2/aggregate-merge.json @@ -0,0 +1,415 @@ +{ + "runOn": [ + { + "minServerVersion": "4.1.11" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "collection_name": "test_aggregate_merge", + "tests": [ + { + "description": "Aggregate with $merge", + "operations": [ + { + "object": "collection", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$merge": { + "into": "other_test_collection" + } + } + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test_aggregate_merge", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$merge": { + "into": "other_test_collection" + } + } + ] + } + } + } + ], + "outcome": { + "collection": { + "name": "other_test_collection", + "data": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "Aggregate with $merge and batch size of 0", + "operations": [ + { + "object": "collection", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$merge": { + "into": "other_test_collection" + } + } + ], + "batchSize": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test_aggregate_merge", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$merge": { + "into": "other_test_collection" + } + } + ], + "cursor": {} + } + } + } + ], + "outcome": { + "collection": { + "name": "other_test_collection", + "data": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "Aggregate with $merge and majority readConcern", + "operations": [ + { + "object": "collection", + "name": "aggregate", + "collectionOptions": { + "readConcern": { + "level": "majority" + } + }, + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$merge": { + "into": "other_test_collection" + } + } + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test_aggregate_merge", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$merge": { + "into": "other_test_collection" + } + } + ], + "readConcern": { + "level": "majority" + } + } + } + } + ], + "outcome": { + "collection": { + "name": "other_test_collection", + "data": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "Aggregate with $merge and local readConcern", + "operations": [ + { + "object": "collection", + "name": "aggregate", + "collectionOptions": { + "readConcern": { + "level": "local" + } + }, + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$merge": { + "into": "other_test_collection" + } + } + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test_aggregate_merge", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$merge": { + "into": "other_test_collection" + } + } + ], + "readConcern": { + "level": "local" + } + } + } + } + ], + "outcome": { + "collection": { + "name": "other_test_collection", + "data": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "Aggregate with $merge and available readConcern", + "operations": [ + { + "object": "collection", + "name": "aggregate", + "collectionOptions": { + "readConcern": { + "level": "available" + } + }, + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$merge": { + "into": "other_test_collection" + } + } + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test_aggregate_merge", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$merge": { + "into": "other_test_collection" + } + } + ], + "readConcern": { + "level": "available" + } + } + } + } + ], + "outcome": { + "collection": { + "name": "other_test_collection", + "data": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/aggregate-out-readConcern.json b/test/crud/v2/aggregate-out-readConcern.json new file mode 100644 index 0000000..c39ee0e --- /dev/null +++ b/test/crud/v2/aggregate-out-readConcern.json @@ -0,0 +1,385 @@ +{ + "runOn": [ + { + "minServerVersion": "4.1.0", + "topology": [ + "replicaset", + "sharded" + ] + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "collection_name": "test_aggregate_out_readconcern", + "tests": [ + { + "description": "readConcern majority with out stage", + "operations": [ + { + "object": "collection", + "name": "aggregate", + "collectionOptions": { + "readConcern": { + "level": "majority" + } + }, + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test_aggregate_out_readconcern", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ], + "readConcern": { + "level": "majority" + } + } + } + } + ], + "outcome": { + "collection": { + "name": "other_test_collection", + "data": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "readConcern local with out stage", + "operations": [ + { + "object": "collection", + "name": "aggregate", + "collectionOptions": { + "readConcern": { + "level": "local" + } + }, + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test_aggregate_out_readconcern", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ], + "readConcern": { + "level": "local" + } + } + } + } + ], + "outcome": { + "collection": { + "name": "other_test_collection", + "data": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "readConcern available with out stage", + "operations": [ + { + "object": "collection", + "name": "aggregate", + "collectionOptions": { + "readConcern": { + "level": "available" + } + }, + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test_aggregate_out_readconcern", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ], + "readConcern": { + "level": "available" + } + } + } + } + ], + "outcome": { + "collection": { + "name": "other_test_collection", + "data": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "readConcern linearizable with out stage", + "operations": [ + { + "object": "collection", + "name": "aggregate", + "collectionOptions": { + "readConcern": { + "level": "linearizable" + } + }, + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ] + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test_aggregate_out_readconcern", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ], + "readConcern": { + "level": "linearizable" + } + } + } + } + ] + }, + { + "description": "invalid readConcern with out stage", + "operations": [ + { + "object": "collection", + "name": "aggregate", + "collectionOptions": { + "readConcern": { + "level": "!invalid123" + } + }, + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ] + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test_aggregate_out_readconcern", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ], + "readConcern": { + "level": "!invalid123" + } + } + } + } + ] + } + ] +} diff --git a/test/crud/v2/bulkWrite-arrayFilters.json b/test/crud/v2/bulkWrite-arrayFilters.json new file mode 100644 index 0000000..2d3ce96 --- /dev/null +++ b/test/crud/v2/bulkWrite-arrayFilters.json @@ -0,0 +1,226 @@ +{ + "runOn": [ + { + "minServerVersion": "3.5.6" + } + ], + "data": [ + { + "_id": 1, + "y": [ + { + "b": 3 + }, + { + "b": 1 + } + ] + }, + { + "_id": 2, + "y": [ + { + "b": 0 + }, + { + "b": 1 + } + ] + } + ], + "collection_name": "test", + "database_name": "crud-tests", + "tests": [ + { + "description": "BulkWrite updateOne with arrayFilters", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateOne", + "arguments": { + "filter": {}, + "update": { + "$set": { + "y.$[i].b": 2 + } + }, + "arrayFilters": [ + { + "i.b": 3 + } + ] + } + } + ], + "options": { + "ordered": true + } + }, + "result": { + "deletedCount": 0, + "insertedCount": 0, + "insertedIds": {}, + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0, + "upsertedIds": {} + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": {}, + "u": { + "$set": { + "y.$[i].b": 2 + } + }, + "arrayFilters": [ + { + "i.b": 3 + } + ] + } + ], + "ordered": true + }, + "command_name": "update", + "database_name": "crud-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "y": [ + { + "b": 2 + }, + { + "b": 1 + } + ] + }, + { + "_id": 2, + "y": [ + { + "b": 0 + }, + { + "b": 1 + } + ] + } + ] + } + } + }, + { + "description": "BulkWrite updateMany with arrayFilters", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateMany", + "arguments": { + "filter": {}, + "update": { + "$set": { + "y.$[i].b": 2 + } + }, + "arrayFilters": [ + { + "i.b": 1 + } + ] + } + } + ], + "options": { + "ordered": true + } + }, + "result": { + "deletedCount": 0, + "insertedCount": 0, + "insertedIds": {}, + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0, + "upsertedIds": {} + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": {}, + "u": { + "$set": { + "y.$[i].b": 2 + } + }, + "multi": true, + "arrayFilters": [ + { + "i.b": 1 + } + ] + } + ], + "ordered": true + }, + "command_name": "update", + "database_name": "crud-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "y": [ + { + "b": 3 + }, + { + "b": 2 + } + ] + }, + { + "_id": 2, + "y": [ + { + "b": 0 + }, + { + "b": 2 + } + ] + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/bulkWrite-delete-hint-clientError.json b/test/crud/v2/bulkWrite-delete-hint-clientError.json new file mode 100644 index 0000000..cfeac90 --- /dev/null +++ b/test/crud/v2/bulkWrite-delete-hint-clientError.json @@ -0,0 +1,150 @@ +{ + "runOn": [ + { + "maxServerVersion": "3.3.99" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ], + "collection_name": "BulkWrite_delete_hint", + "tests": [ + { + "description": "BulkWrite deleteOne with hints unsupported (client-side error)", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + } + }, + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 2 + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "BulkWrite deleteMany with hints unsupported (client-side error)", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "hint": "_id_" + } + }, + { + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gte": 4 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/bulkWrite-delete-hint-serverError.json b/test/crud/v2/bulkWrite-delete-hint-serverError.json new file mode 100644 index 0000000..c68973b --- /dev/null +++ b/test/crud/v2/bulkWrite-delete-hint-serverError.json @@ -0,0 +1,209 @@ +{ + "runOn": [ + { + "minServerVersion": "3.4.0", + "maxServerVersion": "4.3.3" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ], + "collection_name": "BulkWrite_delete_hint", + "tests": [ + { + "description": "BulkWrite deleteOne with hints unsupported (server-side error)", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + } + }, + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 2 + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "BulkWrite_delete_hint", + "deletes": [ + { + "q": { + "_id": 1 + }, + "hint": "_id_", + "limit": 1 + }, + { + "q": { + "_id": 2 + }, + "hint": { + "_id": 1 + }, + "limit": 1 + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "BulkWrite deleteMany with hints unsupported (server-side error)", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "hint": "_id_" + } + }, + { + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gte": 4 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "BulkWrite_delete_hint", + "deletes": [ + { + "q": { + "_id": { + "$lt": 3 + } + }, + "hint": "_id_", + "limit": 0 + }, + { + "q": { + "_id": { + "$gte": 4 + } + }, + "hint": { + "_id": 1 + }, + "limit": 0 + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/bulkWrite-delete-hint.json b/test/crud/v2/bulkWrite-delete-hint.json new file mode 100644 index 0000000..ece3238 --- /dev/null +++ b/test/crud/v2/bulkWrite-delete-hint.json @@ -0,0 +1,204 @@ +{ + "runOn": [ + { + "minServerVersion": "4.3.4" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ], + "collection_name": "BulkWrite_delete_hint", + "tests": [ + { + "description": "BulkWrite deleteOne with hints", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + } + }, + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 2 + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "result": { + "deletedCount": 2, + "insertedCount": 0, + "insertedIds": {}, + "matchedCount": 0, + "modifiedCount": 0, + "upsertedCount": 0, + "upsertedIds": {} + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "BulkWrite_delete_hint", + "deletes": [ + { + "q": { + "_id": 1 + }, + "hint": "_id_", + "limit": 1 + }, + { + "q": { + "_id": 2 + }, + "hint": { + "_id": 1 + }, + "limit": 1 + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "BulkWrite deleteMany with hints", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "hint": "_id_" + } + }, + { + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gte": 4 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "result": { + "deletedCount": 3, + "insertedCount": 0, + "insertedIds": {}, + "matchedCount": 0, + "modifiedCount": 0, + "upsertedCount": 0, + "upsertedIds": {} + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "BulkWrite_delete_hint", + "deletes": [ + { + "q": { + "_id": { + "$lt": 3 + } + }, + "hint": "_id_", + "limit": 0 + }, + { + "q": { + "_id": { + "$gte": 4 + } + }, + "hint": { + "_id": 1 + }, + "limit": 0 + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/bulkWrite-update-hint-clientError.json b/test/crud/v2/bulkWrite-update-hint-clientError.json new file mode 100644 index 0000000..fa919ec --- /dev/null +++ b/test/crud/v2/bulkWrite-update-hint-clientError.json @@ -0,0 +1,235 @@ +{ + "runOn": [ + { + "maxServerVersion": "3.3.99" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ], + "collection_name": "test_bulkwrite_update_hint", + "tests": [ + { + "description": "BulkWrite updateOne with update hints unsupported (client-side error)", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "BulkWrite updateMany with update hints unsupported (client-side error)", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + }, + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "BulkWrite replaceOne with update hints unsupported (client-side error)", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 3 + }, + "replacement": { + "x": 333 + }, + "hint": "_id_" + } + }, + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 4 + }, + "replacement": { + "x": 444 + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/bulkWrite-update-hint-serverError.json b/test/crud/v2/bulkWrite-update-hint-serverError.json new file mode 100644 index 0000000..e8b96ff --- /dev/null +++ b/test/crud/v2/bulkWrite-update-hint-serverError.json @@ -0,0 +1,343 @@ +{ + "runOn": [ + { + "minServerVersion": "3.4.0", + "maxServerVersion": "4.1.9" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ], + "collection_name": "test_bulkwrite_update_hint", + "tests": [ + { + "description": "BulkWrite updateOne with update hints unsupported (server-side error)", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_bulkwrite_update_hint", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "BulkWrite updateMany with update hints unsupported (server-side error)", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + }, + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_bulkwrite_update_hint", + "updates": [ + { + "q": { + "_id": { + "$lt": 3 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": "_id_" + }, + { + "q": { + "_id": { + "$lt": 3 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": { + "_id": 1 + } + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "BulkWrite replaceOne with update hints unsupported (server-side error)", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 3 + }, + "replacement": { + "x": 333 + }, + "hint": "_id_" + } + }, + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 4 + }, + "replacement": { + "x": 444 + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_bulkwrite_update_hint", + "updates": [ + { + "q": { + "_id": 3 + }, + "u": { + "x": 333 + }, + "hint": "_id_" + }, + { + "q": { + "_id": 4 + }, + "u": { + "x": 444 + }, + "hint": { + "_id": 1 + } + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/bulkWrite-update-hint.json b/test/crud/v2/bulkWrite-update-hint.json new file mode 100644 index 0000000..15e169f --- /dev/null +++ b/test/crud/v2/bulkWrite-update-hint.json @@ -0,0 +1,366 @@ +{ + "runOn": [ + { + "minServerVersion": "4.2.0" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ], + "collection_name": "test_bulkwrite_update_hint", + "tests": [ + { + "description": "BulkWrite updateOne with update hints", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "result": { + "deletedCount": 0, + "insertedCount": 0, + "insertedIds": {}, + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0, + "upsertedIds": {} + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_bulkwrite_update_hint", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 13 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "BulkWrite updateMany with update hints", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + }, + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "result": { + "deletedCount": 0, + "insertedCount": 0, + "insertedIds": {}, + "matchedCount": 4, + "modifiedCount": 4, + "upsertedCount": 0, + "upsertedIds": {} + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_bulkwrite_update_hint", + "updates": [ + { + "q": { + "_id": { + "$lt": 3 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": "_id_" + }, + { + "q": { + "_id": { + "$lt": 3 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": { + "_id": 1 + } + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 13 + }, + { + "_id": 2, + "x": 24 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "BulkWrite replaceOne with update hints", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 3 + }, + "replacement": { + "x": 333 + }, + "hint": "_id_" + } + }, + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 4 + }, + "replacement": { + "x": 444 + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "result": { + "deletedCount": 0, + "insertedCount": 0, + "insertedIds": {}, + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0, + "upsertedIds": {} + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_bulkwrite_update_hint", + "updates": [ + { + "q": { + "_id": 3 + }, + "u": { + "x": 333 + }, + "hint": "_id_" + }, + { + "q": { + "_id": 4 + }, + "u": { + "x": 444 + }, + "hint": { + "_id": 1 + } + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 333 + }, + { + "_id": 4, + "x": 444 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/db-aggregate.json b/test/crud/v2/db-aggregate.json new file mode 100644 index 0000000..d88b9e1 --- /dev/null +++ b/test/crud/v2/db-aggregate.json @@ -0,0 +1,81 @@ +{ + "runOn": [ + { + "minServerVersion": "3.6.0" + } + ], + "database_name": "admin", + "tests": [ + { + "description": "Aggregate with $listLocalSessions", + "operations": [ + { + "name": "aggregate", + "object": "database", + "arguments": { + "pipeline": [ + { + "$listLocalSessions": {} + }, + { + "$limit": 1 + }, + { + "$addFields": { + "dummy": "dummy field" + } + }, + { + "$project": { + "_id": 0, + "dummy": 1 + } + } + ] + }, + "result": [ + { + "dummy": "dummy field" + } + ] + } + ] + }, + { + "description": "Aggregate with $listLocalSessions and allowDiskUse", + "operations": [ + { + "name": "aggregate", + "object": "database", + "arguments": { + "pipeline": [ + { + "$listLocalSessions": {} + }, + { + "$limit": 1 + }, + { + "$addFields": { + "dummy": "dummy field" + } + }, + { + "$project": { + "_id": 0, + "dummy": 1 + } + } + ], + "allowDiskUse": true + }, + "result": [ + { + "dummy": "dummy field" + } + ] + } + ] + } + ] +} diff --git a/test/crud/v2/deleteMany-hint-clientError.json b/test/crud/v2/deleteMany-hint-clientError.json new file mode 100644 index 0000000..3a0d025 --- /dev/null +++ b/test/crud/v2/deleteMany-hint-clientError.json @@ -0,0 +1,100 @@ +{ + "runOn": [ + { + "maxServerVersion": "3.3.99" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "collection_name": "DeleteMany_hint", + "tests": [ + { + "description": "DeleteMany with hint string unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "DeleteMany with hint document unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/deleteMany-hint-serverError.json b/test/crud/v2/deleteMany-hint-serverError.json new file mode 100644 index 0000000..5829e86 --- /dev/null +++ b/test/crud/v2/deleteMany-hint-serverError.json @@ -0,0 +1,141 @@ +{ + "runOn": [ + { + "minServerVersion": "3.4.0", + "maxServerVersion": "4.3.3" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "collection_name": "DeleteMany_hint", + "tests": [ + { + "description": "DeleteMany with hint string unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "DeleteMany_hint", + "deletes": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "hint": "_id_", + "limit": 0 + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "DeleteMany with hint document unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "DeleteMany_hint", + "deletes": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "hint": { + "_id": 1 + }, + "limit": 0 + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/deleteMany-hint.json b/test/crud/v2/deleteMany-hint.json new file mode 100644 index 0000000..51ee386 --- /dev/null +++ b/test/crud/v2/deleteMany-hint.json @@ -0,0 +1,128 @@ +{ + "runOn": [ + { + "minServerVersion": "4.3.4" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "collection_name": "DeleteMany_hint", + "tests": [ + { + "description": "DeleteMany with hint string", + "operations": [ + { + "object": "collection", + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "hint": "_id_" + }, + "result": { + "deletedCount": 2 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "DeleteMany_hint", + "deletes": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "hint": "_id_", + "limit": 0 + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + } + ] + } + } + }, + { + "description": "DeleteMany with hint document", + "operations": [ + { + "object": "collection", + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "result": { + "deletedCount": 2 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "DeleteMany_hint", + "deletes": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "hint": { + "_id": 1 + }, + "limit": 0 + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/deleteOne-hint-clientError.json b/test/crud/v2/deleteOne-hint-clientError.json new file mode 100644 index 0000000..97f8ec4 --- /dev/null +++ b/test/crud/v2/deleteOne-hint-clientError.json @@ -0,0 +1,84 @@ +{ + "runOn": [ + { + "maxServerVersion": "3.3.99" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "DeleteOne_hint", + "tests": [ + { + "description": "DeleteOne with hint string unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "DeleteOne with hint document unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/deleteOne-hint-serverError.json b/test/crud/v2/deleteOne-hint-serverError.json new file mode 100644 index 0000000..3cf9400 --- /dev/null +++ b/test/crud/v2/deleteOne-hint-serverError.json @@ -0,0 +1,121 @@ +{ + "runOn": [ + { + "minServerVersion": "3.4.0", + "maxServerVersion": "4.3.3" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "DeleteOne_hint", + "tests": [ + { + "description": "DeleteOne with hint string unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "DeleteOne_hint", + "deletes": [ + { + "q": { + "_id": 1 + }, + "hint": "_id_", + "limit": 1 + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "DeleteOne with hint document unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "DeleteOne_hint", + "deletes": [ + { + "q": { + "_id": 1 + }, + "hint": { + "_id": 1 + }, + "limit": 1 + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/deleteOne-hint.json b/test/crud/v2/deleteOne-hint.json new file mode 100644 index 0000000..ec8e771 --- /dev/null +++ b/test/crud/v2/deleteOne-hint.json @@ -0,0 +1,116 @@ +{ + "runOn": [ + { + "minServerVersion": "4.3.4" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "DeleteOne_hint", + "tests": [ + { + "description": "DeleteOne with hint string", + "operations": [ + { + "object": "collection", + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + }, + "result": { + "deletedCount": 1 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "DeleteOne_hint", + "deletes": [ + { + "q": { + "_id": 1 + }, + "hint": "_id_", + "limit": 1 + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "deleteOne with hint document", + "operations": [ + { + "object": "collection", + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": { + "_id": 1 + } + }, + "result": { + "deletedCount": 1 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "DeleteOne_hint", + "deletes": [ + { + "q": { + "_id": 1 + }, + "hint": { + "_id": 1 + }, + "limit": 1 + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/find-allowdiskuse-clientError.json b/test/crud/v2/find-allowdiskuse-clientError.json new file mode 100644 index 0000000..5ea0139 --- /dev/null +++ b/test/crud/v2/find-allowdiskuse-clientError.json @@ -0,0 +1,40 @@ +{ + "runOn": [ + { + "maxServerVersion": "3.0.99" + } + ], + "collection_name": "test_find_allowdiskuse_clienterror", + "tests": [ + { + "description": "Find fails when allowDiskUse true is specified against pre 3.2 server", + "operations": [ + { + "object": "collection", + "name": "find", + "arguments": { + "filter": {}, + "allowDiskUse": true + }, + "error": true + } + ], + "expectations": [] + }, + { + "description": "Find fails when allowDiskUse false is specified against pre 3.2 server", + "operations": [ + { + "object": "collection", + "name": "find", + "arguments": { + "filter": {}, + "allowDiskUse": false + }, + "error": true + } + ], + "expectations": [] + } + ] +} diff --git a/test/crud/v2/find-allowdiskuse-serverError.json b/test/crud/v2/find-allowdiskuse-serverError.json new file mode 100644 index 0000000..31aa50e --- /dev/null +++ b/test/crud/v2/find-allowdiskuse-serverError.json @@ -0,0 +1,61 @@ +{ + "runOn": [ + { + "minServerVersion": "3.2", + "maxServerVersion": "4.3.0" + } + ], + "collection_name": "test_find_allowdiskuse_servererror", + "tests": [ + { + "description": "Find fails when allowDiskUse true is specified against pre 4.4 server (server-side error)", + "operations": [ + { + "object": "collection", + "name": "find", + "arguments": { + "filter": {}, + "allowDiskUse": true + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "find": "test_find_allowdiskuse_servererror", + "filter": {}, + "allowDiskUse": true + } + } + } + ] + }, + { + "description": "Find fails when allowDiskUse false is specified against pre 4.4 server (server-side error)", + "operations": [ + { + "object": "collection", + "name": "find", + "arguments": { + "filter": {}, + "allowDiskUse": false + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "find": "test_find_allowdiskuse_servererror", + "filter": {}, + "allowDiskUse": false + } + } + } + ] + } + ] +} diff --git a/test/crud/v2/find-allowdiskuse.json b/test/crud/v2/find-allowdiskuse.json new file mode 100644 index 0000000..2df4dbc --- /dev/null +++ b/test/crud/v2/find-allowdiskuse.json @@ -0,0 +1,78 @@ +{ + "runOn": [ + { + "minServerVersion": "4.3.1" + } + ], + "collection_name": "test_find_allowdiskuse", + "tests": [ + { + "description": "Find does not send allowDiskuse when value is not specified", + "operations": [ + { + "object": "collection", + "name": "find", + "arguments": { + "filter": {} + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "find": "test_find_allowdiskuse", + "allowDiskUse": null + } + } + } + ] + }, + { + "description": "Find sends allowDiskuse false when false is specified", + "operations": [ + { + "object": "collection", + "name": "find", + "arguments": { + "filter": {}, + "allowDiskUse": false + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "find": "test_find_allowdiskuse", + "allowDiskUse": false + } + } + } + ] + }, + { + "description": "Find sends allowDiskUse true when true is specified", + "operations": [ + { + "object": "collection", + "name": "find", + "arguments": { + "filter": {}, + "allowDiskUse": true + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "find": "test_find_allowdiskuse", + "allowDiskUse": true + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/test/crud/v2/findOneAndDelete-hint-clientError.json b/test/crud/v2/findOneAndDelete-hint-clientError.json new file mode 100644 index 0000000..262e78c --- /dev/null +++ b/test/crud/v2/findOneAndDelete-hint-clientError.json @@ -0,0 +1,84 @@ +{ + "runOn": [ + { + "maxServerVersion": "4.0.99" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndDelete_hint", + "tests": [ + { + "description": "FindOneAndDelete with hint string unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "findOneAndDelete", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndDelete with hint document", + "operations": [ + { + "object": "collection", + "name": "findOneAndDelete", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/findOneAndDelete-hint-serverError.json b/test/crud/v2/findOneAndDelete-hint-serverError.json new file mode 100644 index 0000000..5d1dd89 --- /dev/null +++ b/test/crud/v2/findOneAndDelete-hint-serverError.json @@ -0,0 +1,113 @@ +{ + "runOn": [ + { + "minServerVersion": "4.2.0", + "maxServerVersion": "4.3.3" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndDelete_hint", + "tests": [ + { + "description": "FindOneAndDelete with hint string unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "findOneAndDelete", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndDelete_hint", + "query": { + "_id": 1 + }, + "hint": "_id_", + "remove": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndDelete with hint document", + "operations": [ + { + "object": "collection", + "name": "findOneAndDelete", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndDelete_hint", + "query": { + "_id": 1 + }, + "hint": { + "_id": 1 + }, + "remove": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/findOneAndDelete-hint.json b/test/crud/v2/findOneAndDelete-hint.json new file mode 100644 index 0000000..fe8dcfa --- /dev/null +++ b/test/crud/v2/findOneAndDelete-hint.json @@ -0,0 +1,110 @@ +{ + "runOn": [ + { + "minServerVersion": "4.3.4" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndDelete_hint", + "tests": [ + { + "description": "FindOneAndDelete with hint string", + "operations": [ + { + "object": "collection", + "name": "findOneAndDelete", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + }, + "result": { + "_id": 1, + "x": 11 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndDelete_hint", + "query": { + "_id": 1 + }, + "hint": "_id_", + "remove": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndDelete with hint document", + "operations": [ + { + "object": "collection", + "name": "findOneAndDelete", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": { + "_id": 1 + } + }, + "result": { + "_id": 1, + "x": 11 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndDelete_hint", + "query": { + "_id": 1 + }, + "hint": { + "_id": 1 + }, + "remove": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/findOneAndReplace-hint-clientError.json b/test/crud/v2/findOneAndReplace-hint-clientError.json new file mode 100644 index 0000000..08fd4b3 --- /dev/null +++ b/test/crud/v2/findOneAndReplace-hint-clientError.json @@ -0,0 +1,90 @@ +{ + "runOn": [ + { + "maxServerVersion": "4.0.99" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndReplace_hint", + "tests": [ + { + "description": "FindOneAndReplace with hint string unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndReplace with hint document unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/findOneAndReplace-hint-serverError.json b/test/crud/v2/findOneAndReplace-hint-serverError.json new file mode 100644 index 0000000..6710e6a --- /dev/null +++ b/test/crud/v2/findOneAndReplace-hint-serverError.json @@ -0,0 +1,123 @@ +{ + "runOn": [ + { + "minServerVersion": "4.2.0", + "maxServerVersion": "4.3.0" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndReplace_hint", + "tests": [ + { + "description": "FindOneAndReplace with hint string unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndReplace_hint", + "query": { + "_id": 1 + }, + "update": { + "x": 33 + }, + "hint": "_id_" + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndReplace with hint document unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndReplace_hint", + "query": { + "_id": 1 + }, + "update": { + "x": 33 + }, + "hint": { + "_id": 1 + } + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/findOneAndReplace-hint.json b/test/crud/v2/findOneAndReplace-hint.json new file mode 100644 index 0000000..263fdf9 --- /dev/null +++ b/test/crud/v2/findOneAndReplace-hint.json @@ -0,0 +1,128 @@ +{ + "runOn": [ + { + "minServerVersion": "4.3.1" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndReplace_hint", + "tests": [ + { + "description": "FindOneAndReplace with hint string", + "operations": [ + { + "object": "collection", + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + }, + "hint": "_id_" + }, + "result": { + "_id": 1, + "x": 11 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndReplace_hint", + "query": { + "_id": 1 + }, + "update": { + "x": 33 + }, + "hint": "_id_" + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 33 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndReplace with hint document", + "operations": [ + { + "object": "collection", + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + }, + "hint": { + "_id": 1 + } + }, + "result": { + "_id": 1, + "x": 11 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndReplace_hint", + "query": { + "_id": 1 + }, + "update": { + "x": 33 + }, + "hint": { + "_id": 1 + } + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 33 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/findOneAndUpdate-hint-clientError.json b/test/crud/v2/findOneAndUpdate-hint-clientError.json new file mode 100644 index 0000000..8cd5cdd --- /dev/null +++ b/test/crud/v2/findOneAndUpdate-hint-clientError.json @@ -0,0 +1,94 @@ +{ + "runOn": [ + { + "maxServerVersion": "4.0.99" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndUpdate_hint", + "tests": [ + { + "description": "FindOneAndUpdate with hint string unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndUpdate with hint document unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/findOneAndUpdate-hint-serverError.json b/test/crud/v2/findOneAndUpdate-hint-serverError.json new file mode 100644 index 0000000..1f4b2bd --- /dev/null +++ b/test/crud/v2/findOneAndUpdate-hint-serverError.json @@ -0,0 +1,131 @@ +{ + "runOn": [ + { + "minServerVersion": "4.2.0", + "maxServerVersion": "4.3.0" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndUpdate_hint", + "tests": [ + { + "description": "FindOneAndUpdate with hint string unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndUpdate_hint", + "query": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndUpdate with hint document unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndUpdate_hint", + "query": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/findOneAndUpdate-hint.json b/test/crud/v2/findOneAndUpdate-hint.json new file mode 100644 index 0000000..451eecc --- /dev/null +++ b/test/crud/v2/findOneAndUpdate-hint.json @@ -0,0 +1,136 @@ +{ + "runOn": [ + { + "minServerVersion": "4.3.1" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndUpdate_hint", + "tests": [ + { + "description": "FindOneAndUpdate with hint string", + "operations": [ + { + "object": "collection", + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "result": { + "_id": 1, + "x": 11 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndUpdate_hint", + "query": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndUpdate with hint document", + "operations": [ + { + "object": "collection", + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "result": { + "_id": 1, + "x": 11 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndUpdate_hint", + "query": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/replaceOne-hint.json b/test/crud/v2/replaceOne-hint.json new file mode 100644 index 0000000..de4aa4d --- /dev/null +++ b/test/crud/v2/replaceOne-hint.json @@ -0,0 +1,146 @@ +{ + "runOn": [ + { + "minServerVersion": "4.2.0" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "test_replaceone_hint", + "tests": [ + { + "description": "ReplaceOne with hint string", + "operations": [ + { + "object": "collection", + "name": "replaceOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "replacement": { + "x": 111 + }, + "hint": "_id_" + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_replaceone_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 111 + }, + "hint": "_id_" + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 111 + } + ] + } + } + }, + { + "description": "ReplaceOne with hint document", + "operations": [ + { + "object": "collection", + "name": "replaceOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "replacement": { + "x": 111 + }, + "hint": { + "_id": 1 + } + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_replaceone_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 111 + }, + "hint": { + "_id": 1 + } + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 111 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/unacknowledged-bulkWrite-delete-hint-clientError.json b/test/crud/v2/unacknowledged-bulkWrite-delete-hint-clientError.json new file mode 100644 index 0000000..46839db --- /dev/null +++ b/test/crud/v2/unacknowledged-bulkWrite-delete-hint-clientError.json @@ -0,0 +1,155 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ], + "collection_name": "BulkWrite_delete_hint", + "tests": [ + { + "description": "Unacknowledged bulkWrite deleteOne with hints fails with client-side error", + "operations": [ + { + "name": "bulkWrite", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "arguments": { + "requests": [ + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + } + }, + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 2 + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "Unacknowledged bulkWrite deleteMany with hints fails with client-side error", + "operations": [ + { + "name": "bulkWrite", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "arguments": { + "requests": [ + { + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "hint": "_id_" + } + }, + { + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gte": 4 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/unacknowledged-bulkWrite-update-hint-clientError.json b/test/crud/v2/unacknowledged-bulkWrite-update-hint-clientError.json new file mode 100644 index 0000000..4a41d76 --- /dev/null +++ b/test/crud/v2/unacknowledged-bulkWrite-update-hint-clientError.json @@ -0,0 +1,245 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ], + "collection_name": "Bulkwrite_update_hint", + "tests": [ + { + "description": "Unacknowledged bulkWrite updateOne with hints fails with client-side error", + "operations": [ + { + "name": "bulkWrite", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "arguments": { + "requests": [ + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "Unacknowledged bulkWrite updateMany with hints fails with client-side error", + "operations": [ + { + "name": "bulkWrite", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "arguments": { + "requests": [ + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + }, + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "Unacknowledged bulkWrite replaceOne with hints fails with client-side error", + "operations": [ + { + "name": "bulkWrite", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "arguments": { + "requests": [ + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 3 + }, + "replacement": { + "x": 333 + }, + "hint": "_id_" + } + }, + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 4 + }, + "replacement": { + "x": 444 + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/unacknowledged-deleteMany-hint-clientError.json b/test/crud/v2/unacknowledged-deleteMany-hint-clientError.json new file mode 100644 index 0000000..532f428 --- /dev/null +++ b/test/crud/v2/unacknowledged-deleteMany-hint-clientError.json @@ -0,0 +1,105 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "collection_name": "DeleteMany_hint", + "tests": [ + { + "description": "Unacknowledged deleteMany with hint string fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "Unacknowledged deleteMany with hint document fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/unacknowledged-deleteOne-hint-clientError.json b/test/crud/v2/unacknowledged-deleteOne-hint-clientError.json new file mode 100644 index 0000000..ff3f05e --- /dev/null +++ b/test/crud/v2/unacknowledged-deleteOne-hint-clientError.json @@ -0,0 +1,89 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "DeleteOne_hint", + "tests": [ + { + "description": "Unacknowledged deleteOne with hint string fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "Unacknowledged deleteOne with hint document fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/unacknowledged-findOneAndDelete-hint-clientError.json b/test/crud/v2/unacknowledged-findOneAndDelete-hint-clientError.json new file mode 100644 index 0000000..0769788 --- /dev/null +++ b/test/crud/v2/unacknowledged-findOneAndDelete-hint-clientError.json @@ -0,0 +1,89 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndDelete_hint", + "tests": [ + { + "description": "Unacknowledged findOneAndDelete with hint string fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "findOneAndDelete", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "Unacknowledged findOneAndDelete with hint document fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "findOneAndDelete", + "arguments": { + "filter": { + "_id": 1 + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/unacknowledged-findOneAndReplace-hint-clientError.json b/test/crud/v2/unacknowledged-findOneAndReplace-hint-clientError.json new file mode 100644 index 0000000..38fbc81 --- /dev/null +++ b/test/crud/v2/unacknowledged-findOneAndReplace-hint-clientError.json @@ -0,0 +1,95 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "FindOneAndReplace_hint", + "tests": [ + { + "description": "Unacknowledged findOneAndReplace with hint string fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "Unacknowledged findOneAndReplace with hint document fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/unacknowledged-findOneAndUpdate-hint-clientError.json b/test/crud/v2/unacknowledged-findOneAndUpdate-hint-clientError.json new file mode 100644 index 0000000..615b4c0 --- /dev/null +++ b/test/crud/v2/unacknowledged-findOneAndUpdate-hint-clientError.json @@ -0,0 +1,99 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "FindOneAndUpdate_hint", + "tests": [ + { + "description": "Unacknowledged findOneAndUpdate with hint string fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "Unacknowledged findOneAndUpdate with hint document fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/unacknowledged-replaceOne-hint-clientError.json b/test/crud/v2/unacknowledged-replaceOne-hint-clientError.json new file mode 100644 index 0000000..c4add73 --- /dev/null +++ b/test/crud/v2/unacknowledged-replaceOne-hint-clientError.json @@ -0,0 +1,99 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "ReplaceOne_hint", + "tests": [ + { + "description": "Unacknowledged ReplaceOne with hint string fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "replaceOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "replacement": { + "x": 111 + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "Unacknowledged ReplaceOne with hint document fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "replaceOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "replacement": { + "x": 111 + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/unacknowledged-updateMany-hint-clientError.json b/test/crud/v2/unacknowledged-updateMany-hint-clientError.json new file mode 100644 index 0000000..eaf3efd --- /dev/null +++ b/test/crud/v2/unacknowledged-updateMany-hint-clientError.json @@ -0,0 +1,115 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "collection_name": "Updatemany_hint", + "tests": [ + { + "description": "Unacknowledged updateMany with hint string fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "Unacknowledged updateMany with hint document fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/unacknowledged-updateOne-hint-clientError.json b/test/crud/v2/unacknowledged-updateOne-hint-clientError.json new file mode 100644 index 0000000..1f8f738 --- /dev/null +++ b/test/crud/v2/unacknowledged-updateOne-hint-clientError.json @@ -0,0 +1,103 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "UpdateOne_hint", + "tests": [ + { + "description": "Unacknowledged updateOne with hint string fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "updateOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "Unacknowledged updateOne with hint document fails with client-side error", + "operations": [ + { + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": 0 + } + }, + "name": "updateOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/updateMany-hint-clientError.json b/test/crud/v2/updateMany-hint-clientError.json new file mode 100644 index 0000000..44ebddc --- /dev/null +++ b/test/crud/v2/updateMany-hint-clientError.json @@ -0,0 +1,110 @@ +{ + "runOn": [ + { + "maxServerVersion": "3.3.99" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "collection_name": "test_updatemany_hint", + "tests": [ + { + "description": "UpdateMany with hint string unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "UpdateMany with hint document unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/updateMany-hint-serverError.json b/test/crud/v2/updateMany-hint-serverError.json new file mode 100644 index 0000000..86f2124 --- /dev/null +++ b/test/crud/v2/updateMany-hint-serverError.json @@ -0,0 +1,161 @@ +{ + "runOn": [ + { + "minServerVersion": "3.4.0", + "maxServerVersion": "4.1.9" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "collection_name": "test_updatemany_hint", + "tests": [ + { + "description": "UpdateMany with hint string unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updatemany_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": "_id_" + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "UpdateMany with hint document unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updatemany_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": { + "_id": 1 + } + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/updateMany-hint.json b/test/crud/v2/updateMany-hint.json new file mode 100644 index 0000000..4893489 --- /dev/null +++ b/test/crud/v2/updateMany-hint.json @@ -0,0 +1,168 @@ +{ + "runOn": [ + { + "minServerVersion": "4.2.0" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "collection_name": "test_updatemany_hint", + "tests": [ + { + "description": "UpdateMany with hint string", + "operations": [ + { + "object": "collection", + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "result": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updatemany_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": "_id_" + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 23 + }, + { + "_id": 3, + "x": 34 + } + ] + } + } + }, + { + "description": "UpdateMany with hint document", + "operations": [ + { + "object": "collection", + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "result": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updatemany_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": { + "_id": 1 + } + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 23 + }, + { + "_id": 3, + "x": 34 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/updateOne-hint-clientError.json b/test/crud/v2/updateOne-hint-clientError.json new file mode 100644 index 0000000..82bfe36 --- /dev/null +++ b/test/crud/v2/updateOne-hint-clientError.json @@ -0,0 +1,98 @@ +{ + "runOn": [ + { + "maxServerVersion": "3.3.99" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "test_updateone_hint", + "tests": [ + { + "description": "UpdateOne with hint string unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "updateOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "UpdateOne with hint document unsupported (client-side error)", + "operations": [ + { + "object": "collection", + "name": "updateOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/updateOne-hint-serverError.json b/test/crud/v2/updateOne-hint-serverError.json new file mode 100644 index 0000000..8e8037e --- /dev/null +++ b/test/crud/v2/updateOne-hint-serverError.json @@ -0,0 +1,147 @@ +{ + "runOn": [ + { + "minServerVersion": "3.4.0", + "maxServerVersion": "4.1.9" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "test_updateone_hint", + "tests": [ + { + "description": "UpdateOne with hint string unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "updateOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updateone_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "UpdateOne with hint document unsupported (server-side error)", + "operations": [ + { + "object": "collection", + "name": "updateOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "error": true + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updateone_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/updateOne-hint.json b/test/crud/v2/updateOne-hint.json new file mode 100644 index 0000000..43f76da --- /dev/null +++ b/test/crud/v2/updateOne-hint.json @@ -0,0 +1,154 @@ +{ + "runOn": [ + { + "minServerVersion": "4.2.0" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "test_updateone_hint", + "tests": [ + { + "description": "UpdateOne with hint string", + "operations": [ + { + "object": "collection", + "name": "updateOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updateone_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 23 + } + ] + } + } + }, + { + "description": "UpdateOne with hint document", + "operations": [ + { + "object": "collection", + "name": "updateOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updateone_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 23 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/updateWithPipelines.json b/test/crud/v2/updateWithPipelines.json new file mode 100644 index 0000000..a310f28 --- /dev/null +++ b/test/crud/v2/updateWithPipelines.json @@ -0,0 +1,408 @@ +{ + "runOn": [ + { + "minServerVersion": "4.1.11" + } + ], + "data": [ + { + "_id": 1, + "x": 1, + "y": 1, + "t": { + "u": { + "v": 1 + } + } + }, + { + "_id": 2, + "x": 2, + "y": 1 + } + ], + "collection_name": "test", + "database_name": "crud-tests", + "tests": [ + { + "description": "UpdateOne using pipelines", + "operations": [ + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": [ + { + "$replaceRoot": { + "newRoot": "$t" + } + }, + { + "$addFields": { + "foo": 1 + } + } + ] + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": [ + { + "$replaceRoot": { + "newRoot": "$t" + } + }, + { + "$addFields": { + "foo": 1 + } + } + ] + } + ] + }, + "command_name": "update", + "database_name": "crud-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "u": { + "v": 1 + }, + "foo": 1 + }, + { + "_id": 2, + "x": 2, + "y": 1 + } + ] + } + } + }, + { + "description": "UpdateMany using pipelines", + "operations": [ + { + "name": "updateMany", + "arguments": { + "filter": {}, + "update": [ + { + "$project": { + "x": 1 + } + }, + { + "$addFields": { + "foo": 1 + } + } + ] + }, + "result": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": {}, + "u": [ + { + "$project": { + "x": 1 + } + }, + { + "$addFields": { + "foo": 1 + } + } + ], + "multi": true + } + ] + }, + "command_name": "update", + "database_name": "crud-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 1, + "foo": 1 + }, + { + "_id": 2, + "x": 2, + "foo": 1 + } + ] + } + } + }, + { + "description": "FindOneAndUpdate using pipelines", + "operations": [ + { + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": [ + { + "$project": { + "x": 1 + } + }, + { + "$addFields": { + "foo": 1 + } + } + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "update": [ + { + "$project": { + "x": 1 + } + }, + { + "$addFields": { + "foo": 1 + } + } + ] + }, + "command_name": "findAndModify", + "database_name": "crud-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 1, + "foo": 1 + }, + { + "_id": 2, + "x": 2, + "y": 1 + } + ] + } + } + }, + { + "description": "UpdateOne in bulk write using pipelines", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": [ + { + "$replaceRoot": { + "newRoot": "$t" + } + }, + { + "$addFields": { + "foo": 1 + } + } + ] + } + } + ] + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": [ + { + "$replaceRoot": { + "newRoot": "$t" + } + }, + { + "$addFields": { + "foo": 1 + } + } + ] + } + ] + }, + "command_name": "update", + "database_name": "crud-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "u": { + "v": 1 + }, + "foo": 1 + }, + { + "_id": 2, + "x": 2, + "y": 1 + } + ] + } + } + }, + { + "description": "UpdateMany in bulk write using pipelines", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateMany", + "arguments": { + "filter": {}, + "update": [ + { + "$project": { + "x": 1 + } + }, + { + "$addFields": { + "foo": 1 + } + } + ] + } + } + ] + }, + "result": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": {}, + "u": [ + { + "$project": { + "x": 1 + } + }, + { + "$addFields": { + "foo": 1 + } + } + ], + "multi": true + } + ] + }, + "command_name": "update", + "database_name": "crud-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 1, + "foo": 1 + }, + { + "_id": 2, + "x": 2, + "foo": 1 + } + ] + } + } + } + ] +} diff --git a/test/test_collection.py b/test/test_collection.py index c92b0c8..44e22ab 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -133,10 +133,26 @@ def test_find(self): last_cmd_payload = self.explain.last_cmd_payload self._compare_command_dicts(last_cmd_payload, last_logger_payload) + for _ in self.collection.find({}, limit=10): + pass + last_logger_payload = self.logger.cmd_payload + res = self.explain.find({}, limit=10) + last_cmd_payload = self.explain.last_cmd_payload + self._compare_command_dicts(last_cmd_payload, last_logger_payload) + + + def test_find_one(self): - self.collection.find_one() + self.collection.find_one(projection=['a', 'b.c']) last_logger_payload = self.logger.cmd_payload - res = self.explain.find_one() + res = self.explain.find_one(projection=['a', 'b.c']) + self.assertIn("queryPlanner", res) + last_cmd_payload = self.explain.last_cmd_payload + self._compare_command_dicts(last_cmd_payload, last_logger_payload) + + self.collection.find_one(projection={'a': 1, 'b.c': 1}) + last_logger_payload = self.logger.cmd_payload + res = self.explain.find_one(projection={'a': 1, 'b.c': 1}) self.assertIn("queryPlanner", res) last_cmd_payload = self.explain.last_cmd_payload self._compare_command_dicts(last_cmd_payload, last_logger_payload) @@ -177,8 +193,7 @@ def test_replace_one(self): def test_estimated_document_count(self): self.collection.estimated_document_count() last_logger_payload = self.logger.cmd_payload - res = self.explain.estimated_document_count() - self.assertIn("queryPlanner", res) + self.explain.estimated_document_count() last_cmd_payload = self.explain.last_cmd_payload self._compare_command_dicts(last_cmd_payload, last_logger_payload) diff --git a/test/test_crud_v2.py b/test/test_crud_v2.py new file mode 100644 index 0000000..562e119 --- /dev/null +++ b/test/test_crud_v2.py @@ -0,0 +1,72 @@ +# Copyright 2019-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test the collection module.""" + +import os +import sys + +sys.path[0:0] = [""] + +from test import unittest +from test.utils import TestCreator +from test.utils_spec_runner import SpecRunner + + +# Location of JSON test specifications. +_TEST_PATH = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'crud', 'v2') + +# Default test database and collection names. +TEST_DB = 'testdb' +TEST_COLLECTION = 'testcollection' + + +class TestSpec(SpecRunner): + def get_scenario_db_name(self, scenario_def): + """Crud spec says database_name is optional.""" + return scenario_def.get('database_name', TEST_DB) + + def get_scenario_coll_name(self, scenario_def): + """Crud spec says collection_name is optional.""" + return scenario_def.get('collection_name', TEST_COLLECTION) + + def get_object_name(self, op): + """Crud spec says object is optional and defaults to 'collection'.""" + return op.get('object', 'collection') + + def get_outcome_coll_name(self, outcome, collection): + """Crud spec says outcome has an optional 'collection.name'.""" + return outcome['collection'].get('name', collection.name) + + def setup_scenario(self, scenario_def): + """Allow specs to override a test's setup.""" + # PYTHON-1935 Only create the collection if there is data to insert. + if scenario_def['data']: + super(TestSpec, self).setup_scenario(scenario_def) + + +def create_test(scenario_def, test, name): + def run_scenario(self): + self.run_scenario(scenario_def, test) + + return run_scenario + + +test_creator = TestCreator(create_test, TestSpec, _TEST_PATH) +test_creator.create_tests() + + +if __name__ == "__main__": + unittest.main() diff --git a/test/utils.py b/test/utils.py new file mode 100644 index 0000000..d077c91 --- /dev/null +++ b/test/utils.py @@ -0,0 +1,912 @@ +# Copyright 2012-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for testing pymongo +""" + +import collections +import contextlib +import functools +import os +import re +import shutil +import sys +import threading +import time +import warnings + +from collections import defaultdict +from functools import partial + +from bson import json_util, py3compat +from bson.objectid import ObjectId + +from pymongo import (MongoClient, + monitoring, read_preferences) +from pymongo.errors import ConfigurationError, OperationFailure +from pymongo.monitoring import _SENSITIVE_COMMANDS, ConnectionPoolListener +from pymongo.pool import PoolOptions +from pymongo.read_concern import ReadConcern +from pymongo.read_preferences import ReadPreference +from pymongo.server_selectors import (any_server_selector, + writable_server_selector) +from pymongo.server_type import SERVER_TYPE +from pymongo.write_concern import WriteConcern + +from test import (client_context, + db_user, + db_pwd) + +_SENSITIVE_COMMANDS.add("explain") + +if sys.version_info[0] < 3: + # Python 2.7, use our backport. + from test.barrier import Barrier +else: + from threading import Barrier + + +IMPOSSIBLE_WRITE_CONCERN = WriteConcern(w=50) + + +class CMAPListener(ConnectionPoolListener): + def __init__(self): + self.events = [] + + def reset(self): + self.events = [] + + def add_event(self, event): + self.events.append(event) + + def event_count(self, event_type): + return len([event for event in self.events[:] + if isinstance(event, event_type)]) + + def connection_created(self, event): + self.add_event(event) + + def connection_ready(self, event): + self.add_event(event) + + def connection_closed(self, event): + self.add_event(event) + + def connection_check_out_started(self, event): + self.add_event(event) + + def connection_check_out_failed(self, event): + self.add_event(event) + + def connection_checked_out(self, event): + self.add_event(event) + + def connection_checked_in(self, event): + self.add_event(event) + + def pool_created(self, event): + self.add_event(event) + + def pool_cleared(self, event): + self.add_event(event) + + def pool_closed(self, event): + self.add_event(event) + + +class EventListener(monitoring.CommandListener): + + def __init__(self): + self.results = defaultdict(list) + + def started(self, event): + self.results['started'].append(event) + + def succeeded(self, event): + self.results['succeeded'].append(event) + + def failed(self, event): + self.results['failed'].append(event) + + def started_command_names(self): + """Return list of command names started.""" + return [event.command_name for event in self.results['started']] + + def reset(self): + """Reset the state of this listener.""" + self.results.clear() + + +class WhiteListEventListener(EventListener): + + def __init__(self, *commands): + self.commands = set(commands) + super(WhiteListEventListener, self).__init__() + + def started(self, event): + if event.command_name in self.commands: + super(WhiteListEventListener, self).started(event) + + def succeeded(self, event): + if event.command_name in self.commands: + super(WhiteListEventListener, self).succeeded(event) + + def failed(self, event): + if event.command_name in self.commands: + super(WhiteListEventListener, self).failed(event) + + +class OvertCommandListener(EventListener): + """A CommandListener that ignores sensitive commands.""" + def started(self, event): + if event.command_name.lower() not in _SENSITIVE_COMMANDS: + super(OvertCommandListener, self).started(event) + + def succeeded(self, event): + if event.command_name.lower() not in _SENSITIVE_COMMANDS: + super(OvertCommandListener, self).succeeded(event) + + def failed(self, event): + if event.command_name.lower() not in _SENSITIVE_COMMANDS: + super(OvertCommandListener, self).failed(event) + + +class ServerAndTopologyEventListener(monitoring.ServerListener, + monitoring.TopologyListener): + """Listens to all events.""" + + def __init__(self): + self.results = [] + + def opened(self, event): + self.results.append(event) + + def description_changed(self, event): + self.results.append(event) + + def closed(self, event): + self.results.append(event) + + def matching(self, matcher): + """Return the matching events.""" + results = self.results[:] + return [event for event in results if matcher(event)] + + def reset(self): + self.results = [] + + +class HeartbeatEventListener(monitoring.ServerHeartbeatListener): + """Listens to only server heartbeat events.""" + + def __init__(self): + self.results = [] + + def started(self, event): + self.results.append(event) + + def succeeded(self, event): + self.results.append(event) + + def failed(self, event): + self.results.append(event) + + +class MockSocketInfo(object): + def close(self): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + +class MockPool(object): + def __init__(self, *args, **kwargs): + self.generation = 0 + self._lock = threading.Lock() + self.opts = PoolOptions() + + def get_socket(self, all_credentials): + return MockSocketInfo() + + def return_socket(self, *args, **kwargs): + pass + + def _reset(self): + with self._lock: + self.generation += 1 + + def reset(self): + self._reset() + + def close(self): + self._reset() + + def update_is_writable(self, is_writable): + pass + + def remove_stale_sockets(self, *args, **kwargs): + pass + + +class ScenarioDict(dict): + """Dict that returns {} for any unknown key, recursively.""" + def __init__(self, data): + def convert(v): + if isinstance(v, collections.Mapping): + return ScenarioDict(v) + if isinstance(v, (py3compat.string_type, bytes)): + return v + if isinstance(v, collections.Sequence): + return [convert(item) for item in v] + return v + + dict.__init__(self, [(k, convert(v)) for k, v in data.items()]) + + def __getitem__(self, item): + try: + return dict.__getitem__(self, item) + except KeyError: + # Unlike a defaultdict, don't set the key, just return a dict. + return ScenarioDict({}) + + +class CompareType(object): + """Class that compares equal to any object of the given type.""" + def __init__(self, type): + self.type = type + + def __eq__(self, other): + return isinstance(other, self.type) + + def __ne__(self, other): + """Needed for Python 2.""" + return not self.__eq__(other) + + +class FunctionCallRecorder(object): + """Utility class to wrap a callable and record its invocations.""" + def __init__(self, function): + self._function = function + self._call_list = [] + + def __call__(self, *args, **kwargs): + self._call_list.append((args, kwargs)) + return self._function(*args, **kwargs) + + def reset(self): + """Wipes the call list.""" + self._call_list = [] + + def call_list(self): + """Returns a copy of the call list.""" + return self._call_list[:] + + @property + def call_count(self): + """Returns the number of times the function has been called.""" + return len(self._call_list) + + +class TestCreator(object): + """Class to create test cases from specifications.""" + def __init__(self, create_test, test_class, test_path): + """Create a TestCreator object. + + :Parameters: + - `create_test`: callback that returns a test case. The callback + must accept the following arguments - a dictionary containing the + entire test specification (the `scenario_def`), a dictionary + containing the specification for which the test case will be + generated (the `test_def`). + - `test_class`: the unittest.TestCase class in which to create the + test case. + - `test_path`: path to the directory containing the JSON files with + the test specifications. + """ + self._create_test = create_test + self._test_class = test_class + self.test_path = test_path + + def _ensure_min_max_server_version(self, scenario_def, method): + """Test modifier that enforces a version range for the server on a + test case.""" + if 'minServerVersion' in scenario_def: + min_ver = tuple( + int(elt) for + elt in scenario_def['minServerVersion'].split('.')) + if min_ver is not None: + method = client_context.require_version_min(*min_ver)(method) + + if 'maxServerVersion' in scenario_def: + max_ver = tuple( + int(elt) for + elt in scenario_def['maxServerVersion'].split('.')) + if max_ver is not None: + method = client_context.require_version_max(*max_ver)(method) + + return method + + @staticmethod + def valid_topology(run_on_req): + return client_context.is_topology_type( + run_on_req.get('topology', ['single', 'replicaset', 'sharded'])) + + @staticmethod + def min_server_version(run_on_req): + version = run_on_req.get('minServerVersion') + if version: + min_ver = tuple(int(elt) for elt in version.split('.')) + return client_context.version >= min_ver + return True + + @staticmethod + def max_server_version(run_on_req): + version = run_on_req.get('maxServerVersion') + if version: + max_ver = tuple(int(elt) for elt in version.split('.')) + return client_context.version <= max_ver + return True + + def should_run_on(self, scenario_def): + run_on = scenario_def.get('runOn', []) + if not run_on: + # Always run these tests. + return True + + for req in run_on: + if (self.valid_topology(req) and + self.min_server_version(req) and + self.max_server_version(req)): + return True + return False + + def ensure_run_on(self, scenario_def, method): + """Test modifier that enforces a 'runOn' on a test case.""" + return client_context._require( + lambda: self.should_run_on(scenario_def), + "runOn not satisfied", + method) + + def tests(self, scenario_def): + """Allow CMAP spec test to override the location of test.""" + return scenario_def['tests'] + + def create_tests(self): + for dirpath, _, filenames in os.walk(self.test_path): + dirname = os.path.split(dirpath)[-1] + + for filename in filenames: + with open(os.path.join(dirpath, filename)) as scenario_stream: + # Use tz_aware=False to match how CodecOptions decodes + # dates. + opts = json_util.JSONOptions(tz_aware=False) + scenario_def = ScenarioDict( + json_util.loads(scenario_stream.read(), + json_options=opts)) + + test_type = os.path.splitext(filename)[0] + + # Construct test from scenario. + for test_def in self.tests(scenario_def): + test_name = 'test_%s_%s_%s' % ( + dirname, + test_type.replace("-", "_").replace('.', '_'), + str(test_def['description'].replace(" ", "_").replace( + '.', '_'))) + + new_test = self._create_test( + scenario_def, test_def, test_name) + new_test = self._ensure_min_max_server_version( + scenario_def, new_test) + new_test = self.ensure_run_on( + scenario_def, new_test) + + new_test.__name__ = test_name + setattr(self._test_class, new_test.__name__, new_test) + + +def _connection_string(h, authenticate): + if h.startswith("mongodb://"): + return h + elif client_context.auth_enabled and authenticate: + return "mongodb://%s:%s@%s" % (db_user, db_pwd, str(h)) + else: + return "mongodb://%s" % (str(h),) + + +def _mongo_client(host, port, authenticate=True, directConnection=False, + **kwargs): + """Create a new client over SSL/TLS if necessary.""" + host = host or client_context.host + port = port or client_context.port + client_options = client_context.default_client_options.copy() + if client_context.replica_set_name and not directConnection: + client_options['replicaSet'] = client_context.replica_set_name + client_options.update(kwargs) + + client = MongoClient(_connection_string(host, authenticate), port, + **client_options) + + return client + + +def single_client_noauth(h=None, p=None, **kwargs): + """Make a direct connection. Don't authenticate.""" + return _mongo_client(h, p, authenticate=False, + directConnection=True, **kwargs) + + +def single_client(h=None, p=None, **kwargs): + """Make a direct connection, and authenticate if necessary.""" + return _mongo_client(h, p, directConnection=True, **kwargs) + + +def rs_client_noauth(h=None, p=None, **kwargs): + """Connect to the replica set. Don't authenticate.""" + return _mongo_client(h, p, authenticate=False, **kwargs) + + +def rs_client(h=None, p=None, **kwargs): + """Connect to the replica set and authenticate if necessary.""" + return _mongo_client(h, p, **kwargs) + + +def rs_or_single_client_noauth(h=None, p=None, **kwargs): + """Connect to the replica set if there is one, otherwise the standalone. + + Like rs_or_single_client, but does not authenticate. + """ + return _mongo_client(h, p, authenticate=False, **kwargs) + + +def rs_or_single_client(h=None, p=None, **kwargs): + """Connect to the replica set if there is one, otherwise the standalone. + + Authenticates if necessary. + """ + return _mongo_client(h, p, **kwargs) + + +def ensure_all_connected(client): + """Ensure that the client's connection pool has socket connections to all + members of a replica set. Raises ConfigurationError when called with a + non-replica set client. + + Depending on the use-case, the caller may need to clear any event listeners + that are configured on the client. + """ + ismaster = client.admin.command("isMaster") + if 'setName' not in ismaster: + raise ConfigurationError("cluster is not a replica set") + + target_host_list = set(ismaster['hosts']) + connected_host_list = set([ismaster['me']]) + admindb = client.get_database('admin') + + # Run isMaster until we have connected to each host at least once. + while connected_host_list != target_host_list: + ismaster = admindb.command("isMaster", + read_preference=ReadPreference.SECONDARY) + connected_host_list.update([ismaster["me"]]) + + +def one(s): + """Get one element of a set""" + return next(iter(s)) + + +def oid_generated_on_process(oid): + """Makes a determination as to whether the given ObjectId was generated + by the current process, based on the 5-byte random number in the ObjectId. + """ + return ObjectId._random() == oid.binary[4:9] + + +def delay(sec): + return '''function() { sleep(%f * 1000); return true; }''' % sec + + +def get_command_line(client): + command_line = client.admin.command('getCmdLineOpts') + assert command_line['ok'] == 1, "getCmdLineOpts() failed" + return command_line + + +def camel_to_snake(camel): + # Regex to convert CamelCase to snake_case. + snake = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camel) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', snake).lower() + + +def camel_to_upper_camel(camel): + return camel[0].upper() + camel[1:] + + +def camel_to_snake_args(arguments): + for arg_name in list(arguments): + c2s = camel_to_snake(arg_name) + arguments[c2s] = arguments.pop(arg_name) + return arguments + + +def parse_collection_options(opts): + if 'readPreference' in opts: + opts['read_preference'] = parse_read_preference( + opts.pop('readPreference')) + + if 'writeConcern' in opts: + opts['write_concern'] = WriteConcern( + **dict(opts.pop('writeConcern'))) + + if 'readConcern' in opts: + opts['read_concern'] = ReadConcern( + **dict(opts.pop('readConcern'))) + return opts + + +def server_started_with_option(client, cmdline_opt, config_opt): + """Check if the server was started with a particular option. + + :Parameters: + - `cmdline_opt`: The command line option (i.e. --nojournal) + - `config_opt`: The config file option (i.e. nojournal) + """ + command_line = get_command_line(client) + if 'parsed' in command_line: + parsed = command_line['parsed'] + if config_opt in parsed: + return parsed[config_opt] + argv = command_line['argv'] + return cmdline_opt in argv + + +def server_started_with_auth(client): + try: + command_line = get_command_line(client) + except OperationFailure as e: + msg = e.details.get('errmsg', '') + if e.code == 13 or 'unauthorized' in msg or 'login' in msg: + # Unauthorized. + return True + raise + + # MongoDB >= 2.0 + if 'parsed' in command_line: + parsed = command_line['parsed'] + # MongoDB >= 2.6 + if 'security' in parsed: + security = parsed['security'] + # >= rc3 + if 'authorization' in security: + return security['authorization'] == 'enabled' + # < rc3 + return security.get('auth', False) or bool(security.get('keyFile')) + return parsed.get('auth', False) or bool(parsed.get('keyFile')) + # Legacy + argv = command_line['argv'] + return '--auth' in argv or '--keyFile' in argv + + +def server_started_with_nojournal(client): + command_line = get_command_line(client) + + # MongoDB 2.6. + if 'parsed' in command_line: + parsed = command_line['parsed'] + if 'storage' in parsed: + storage = parsed['storage'] + if 'journal' in storage: + return not storage['journal']['enabled'] + + return server_started_with_option(client, '--nojournal', 'nojournal') + + +def server_is_master_with_slave(client): + command_line = get_command_line(client) + if 'parsed' in command_line: + return command_line['parsed'].get('master', False) + return '--master' in command_line['argv'] + + +def drop_collections(db): + # Drop all non-system collections in this database. + for coll in db.list_collection_names( + filter={"name": {"$regex": r"^(?!system\.)"}}): + db.drop_collection(coll) + + +def remove_all_users(db): + db.command("dropAllUsersFromDatabase", 1, + writeConcern={"w": client_context.w}) + + +def joinall(threads): + """Join threads with a 5-minute timeout, assert joins succeeded""" + for t in threads: + t.join(300) + assert not t.is_alive(), "Thread %s hung" % t + + +def connected(client): + """Convenience to wait for a newly-constructed client to connect.""" + with warnings.catch_warnings(): + # Ignore warning that "ismaster" is always routed to primary even + # if client's read preference isn't PRIMARY. + warnings.simplefilter("ignore", UserWarning) + client.admin.command('ismaster') # Force connection. + + return client + + +def wait_until(predicate, success_description, timeout=10): + """Wait up to 10 seconds (by default) for predicate to be true. + + E.g.: + + wait_until(lambda: client.primary == ('a', 1), + 'connect to the primary') + + If the lambda-expression isn't true after 10 seconds, we raise + AssertionError("Didn't ever connect to the primary"). + + Returns the predicate's first true value. + """ + start = time.time() + interval = min(float(timeout)/100, 0.1) + while True: + retval = predicate() + if retval: + return retval + + if time.time() - start > timeout: + raise AssertionError("Didn't ever %s" % success_description) + + time.sleep(interval) + + +def is_mongos(client): + res = client.admin.command('ismaster') + return res.get('msg', '') == 'isdbgrid' + + +def assertRaisesExactly(cls, fn, *args, **kwargs): + """ + Unlike the standard assertRaises, this checks that a function raises a + specific class of exception, and not a subclass. E.g., check that + MongoClient() raises ConnectionFailure but not its subclass, AutoReconnect. + """ + try: + fn(*args, **kwargs) + except Exception as e: + assert e.__class__ == cls, "got %s, expected %s" % ( + e.__class__.__name__, cls.__name__) + else: + raise AssertionError("%s not raised" % cls) + + +@contextlib.contextmanager +def _ignore_deprecations(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + yield + + +def ignore_deprecations(wrapped=None): + """A context manager or a decorator.""" + if wrapped: + @functools.wraps(wrapped) + def wrapper(*args, **kwargs): + with _ignore_deprecations(): + return wrapped(*args, **kwargs) + + return wrapper + + else: + return _ignore_deprecations() + + +class DeprecationFilter(object): + + def __init__(self, action="ignore"): + """Start filtering deprecations.""" + self.warn_context = warnings.catch_warnings() + self.warn_context.__enter__() + warnings.simplefilter(action, DeprecationWarning) + + def stop(self): + """Stop filtering deprecations.""" + self.warn_context.__exit__() + self.warn_context = None + + +def get_pool(client): + """Get the standalone, primary, or mongos pool.""" + topology = client._get_topology() + server = topology.select_server(writable_server_selector) + return server.pool + + +def get_pools(client): + """Get all pools.""" + return [ + server.pool for server in + client._get_topology().select_servers(any_server_selector)] + + +# Constants for run_threads and lazy_client_trial. +NTRIALS = 5 +NTHREADS = 10 + + +def run_threads(collection, target): + """Run a target function in many threads. + + target is a function taking a Collection and an integer. + """ + threads = [] + for i in range(NTHREADS): + bound_target = partial(target, collection, i) + threads.append(threading.Thread(target=bound_target)) + + for t in threads: + t.start() + + for t in threads: + t.join(60) + assert not t.is_alive() + + +@contextlib.contextmanager +def frequent_thread_switches(): + """Make concurrency bugs more likely to manifest.""" + interval = None + if not sys.platform.startswith('java'): + if hasattr(sys, 'getswitchinterval'): + interval = sys.getswitchinterval() + sys.setswitchinterval(1e-6) + else: + interval = sys.getcheckinterval() + sys.setcheckinterval(1) + + try: + yield + finally: + if not sys.platform.startswith('java'): + if hasattr(sys, 'setswitchinterval'): + sys.setswitchinterval(interval) + else: + sys.setcheckinterval(interval) + + +def lazy_client_trial(reset, target, test, get_client): + """Test concurrent operations on a lazily-connecting client. + + `reset` takes a collection and resets it for the next trial. + + `target` takes a lazily-connecting collection and an index from + 0 to NTHREADS, and performs some operation, e.g. an insert. + + `test` takes the lazily-connecting collection and asserts a + post-condition to prove `target` succeeded. + """ + collection = client_context.client.pymongo_test.test + + with frequent_thread_switches(): + for i in range(NTRIALS): + reset(collection) + lazy_client = get_client() + lazy_collection = lazy_client.pymongo_test.test + run_threads(lazy_collection, target) + test(lazy_collection) + + +def gevent_monkey_patched(): + """Check if gevent's monkey patching is active.""" + # In Python 3.6 importing gevent.socket raises an ImportWarning. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", ImportWarning) + try: + import socket + import gevent.socket + return socket.socket is gevent.socket.socket + except ImportError: + return False + + +def eventlet_monkey_patched(): + """Check if eventlet's monkey patching is active.""" + try: + import threading + import eventlet + return (threading.current_thread.__module__ == + 'eventlet.green.threading') + except ImportError: + return False + + +def is_greenthread_patched(): + return gevent_monkey_patched() or eventlet_monkey_patched() + + +def disable_replication(client): + """Disable replication on all secondaries, requires MongoDB 3.2.""" + for host, port in client.secondaries: + secondary = single_client(host, port) + secondary.admin.command('configureFailPoint', 'stopReplProducer', + mode='alwaysOn') + + +def enable_replication(client): + """Enable replication on all secondaries, requires MongoDB 3.2.""" + for host, port in client.secondaries: + secondary = single_client(host, port) + secondary.admin.command('configureFailPoint', 'stopReplProducer', + mode='off') + + +class ExceptionCatchingThread(threading.Thread): + """A thread that stores any exception encountered from run().""" + def __init__(self, *args, **kwargs): + self.exc = None + super(ExceptionCatchingThread, self).__init__(*args, **kwargs) + + def run(self): + try: + super(ExceptionCatchingThread, self).run() + except BaseException as exc: + self.exc = exc + raise + + +def parse_read_preference(pref): + # Make first letter lowercase to match read_pref's modes. + mode_string = pref.get('mode', 'primary') + mode_string = mode_string[:1].lower() + mode_string[1:] + mode = read_preferences.read_pref_mode_from_name(mode_string) + max_staleness = pref.get('maxStalenessSeconds', -1) + tag_sets = pref.get('tag_sets') + return read_preferences.make_read_preference( + mode, tag_sets=tag_sets, max_staleness=max_staleness) + + +def server_name_to_type(name): + """Convert a ServerType name to the corresponding value. For SDAM tests.""" + # Special case, some tests in the spec include the PossiblePrimary + # type, but only single-threaded drivers need that type. We call + # possible primaries Unknown. + if name == 'PossiblePrimary': + return SERVER_TYPE.Unknown + return getattr(SERVER_TYPE, name) + + +def cat_files(dest, *sources): + """Cat multiple files into dest.""" + with open(dest, 'wb') as fdst: + for src in sources: + with open(src, 'rb') as fsrc: + shutil.copyfileobj(fsrc, fdst) + + +@contextlib.contextmanager +def assertion_context(msg): + """A context manager that adds info to an assertion failure.""" + try: + yield + except AssertionError as exc: + msg = '%s (%s)' % (exc, msg) + py3compat.reraise(type(exc), msg, sys.exc_info()[2]) diff --git a/test/utils_spec_runner.py b/test/utils_spec_runner.py new file mode 100644 index 0000000..cc2b022 --- /dev/null +++ b/test/utils_spec_runner.py @@ -0,0 +1,726 @@ +# Copyright 2020-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for testing driver specs.""" + +import copy +import sys + +from pymongoexplain.explainable_collection import ExplainCollection, Document +from pymongo import monitoring + +class CommandLogger(monitoring.CommandListener): + def __init__(self): + self.cmd_payload = {} + def started(self, event): + self.cmd_payload = event.command + + def succeeded(self, event): + pass + + def failed(self, event): + pass + +from bson import decode, encode +from bson.binary import Binary, STANDARD +from bson.codec_options import CodecOptions +from bson.int64 import Int64 +from bson.py3compat import iteritems, abc, string_type, text_type +from bson.son import SON + +from gridfs import GridFSBucket + +from pymongo import (client_session, + helpers, + operations) +from pymongo.command_cursor import CommandCursor +from pymongo.cursor import Cursor +from pymongo.errors import (BulkWriteError, + OperationFailure, + PyMongoError) +from pymongo.read_concern import ReadConcern +from pymongo.read_preferences import ReadPreference +from pymongo.results import _WriteResult, BulkWriteResult +from pymongo.write_concern import WriteConcern + +from test import (client_context, + client_knobs, + IntegrationTest, + unittest) +from test.utils import (camel_to_snake, + camel_to_snake_args, + camel_to_upper_camel, + CompareType, + OvertCommandListener, + rs_client, parse_read_preference) + + +class SpecRunner(IntegrationTest): + + @classmethod + def setUpClass(cls): + super(SpecRunner, cls).setUpClass() + cls.mongos_clients = [] + + # Speed up the tests by decreasing the heartbeat frequency. + cls.knobs = client_knobs(min_heartbeat_interval=0.1) + cls.knobs.enable() + + @classmethod + def tearDownClass(cls): + cls.knobs.disable() + super(SpecRunner, cls).tearDownClass() + + def setUp(self): + super(SpecRunner, self).setUp() + self.listener = None + self.maxDiff = None + + def _set_fail_point(self, client, command_args): + cmd = SON([('configureFailPoint', 'failCommand')]) + cmd.update(command_args) + client.admin.command(cmd) + + def set_fail_point(self, command_args): + cmd = SON([('configureFailPoint', 'failCommand')]) + cmd.update(command_args) + clients = self.mongos_clients if self.mongos_clients else [self.client] + for client in clients: + self._set_fail_point(client, cmd) + + def targeted_fail_point(self, session, fail_point): + """Run the targetedFailPoint test operation. + + Enable the fail point on the session's pinned mongos. + """ + clients = {c.address: c for c in self.mongos_clients} + client = clients[session._pinned_address] + self._set_fail_point(client, fail_point) + self.addCleanup(self.set_fail_point, {'mode': 'off'}) + + def assert_session_pinned(self, session): + """Run the assertSessionPinned test operation. + + Assert that the given session is pinned. + """ + self.assertIsNotNone(session._transaction.pinned_address) + + def assert_session_unpinned(self, session): + """Run the assertSessionUnpinned test operation. + + Assert that the given session is not pinned. + """ + self.assertIsNone(session._pinned_address) + self.assertIsNone(session._transaction.pinned_address) + + def assert_collection_exists(self, database, collection): + """Run the assertCollectionExists test operation.""" + db = self.client[database] + self.assertIn(collection, db.list_collection_names()) + + def assert_collection_not_exists(self, database, collection): + """Run the assertCollectionNotExists test operation.""" + db = self.client[database] + self.assertNotIn(collection, db.list_collection_names()) + + def assert_index_exists(self, database, collection, index): + """Run the assertIndexExists test operation.""" + coll = self.client[database][collection] + self.assertIn(index, [doc['name'] for doc in coll.list_indexes()]) + + def assert_index_not_exists(self, database, collection, index): + """Run the assertIndexNotExists test operation.""" + coll = self.client[database][collection] + self.assertNotIn(index, [doc['name'] for doc in coll.list_indexes()]) + + def assertErrorLabelsContain(self, exc, expected_labels): + labels = [l for l in expected_labels if exc.has_error_label(l)] + self.assertEqual(labels, expected_labels) + + def assertErrorLabelsOmit(self, exc, omit_labels): + for label in omit_labels: + self.assertFalse( + exc.has_error_label(label), + msg='error labels should not contain %s' % (label,)) + + def kill_all_sessions(self): + clients = self.mongos_clients if self.mongos_clients else [self.client] + for client in clients: + try: + client.admin.command('killAllSessions', []) + except OperationFailure: + # "operation was interrupted" by killing the command's + # own session. + pass + + def check_command_result(self, expected_result, result): + # Only compare the keys in the expected result. + filtered_result = {} + for key in expected_result: + try: + filtered_result[key] = result[key] + except KeyError: + pass + self.assertEqual(filtered_result, expected_result) + + # TODO: factor the following function with test_crud.py. + def check_result(self, expected_result, result): + if isinstance(result, _WriteResult): + for res in expected_result: + prop = camel_to_snake(res) + # SPEC-869: Only BulkWriteResult has upserted_count. + if (prop == "upserted_count" + and not isinstance(result, BulkWriteResult)): + if result.upserted_id is not None: + upserted_count = 1 + else: + upserted_count = 0 + self.assertEqual(upserted_count, expected_result[res], prop) + elif prop == "inserted_ids": + # BulkWriteResult does not have inserted_ids. + if isinstance(result, BulkWriteResult): + self.assertEqual(len(expected_result[res]), + result.inserted_count) + else: + # InsertManyResult may be compared to [id1] from the + # crud spec or {"0": id1} from the retryable write spec. + ids = expected_result[res] + if isinstance(ids, dict): + ids = [ids[str(i)] for i in range(len(ids))] + self.assertEqual(ids, result.inserted_ids, prop) + elif prop == "upserted_ids": + # Convert indexes from strings to integers. + ids = expected_result[res] + expected_ids = {} + for str_index in ids: + expected_ids[int(str_index)] = ids[str_index] + self.assertEqual(expected_ids, result.upserted_ids, prop) + else: + self.assertEqual( + getattr(result, prop), expected_result[res], prop) + + return True + else: + self.assertEqual(result, expected_result) + + def get_object_name(self, op): + """Allow subclasses to override handling of 'object' + + Transaction spec says 'object' is required. + """ + return op['object'] + + @staticmethod + def parse_options(opts): + if 'readPreference' in opts: + opts['read_preference'] = parse_read_preference( + opts.pop('readPreference')) + + if 'writeConcern' in opts: + opts['write_concern'] = WriteConcern( + **dict(opts.pop('writeConcern'))) + + if 'readConcern' in opts: + opts['read_concern'] = ReadConcern( + **dict(opts.pop('readConcern'))) + + if 'maxTimeMS' in opts: + opts['max_time_ms'] = opts.pop('maxTimeMS') + + if 'maxCommitTimeMS' in opts: + opts['max_commit_time_ms'] = opts.pop('maxCommitTimeMS') + + if 'hint' in opts: + hint = opts.pop('hint') + if not isinstance(hint, string_type): + hint = list(iteritems(hint)) + opts['hint'] = hint + + # Properly format 'hint' arguments for the Bulk API tests. + if 'requests' in opts: + reqs = opts.pop('requests') + for req in reqs: + args = req.pop('arguments') + if 'hint' in args: + hint = args.pop('hint') + if not isinstance(hint, string_type): + hint = list(iteritems(hint)) + args['hint'] = hint + req['arguments'] = args + opts['requests'] = reqs + + return dict(opts) + + def _compare_command_dicts(self, ours, theirs): + print(ours) + print(theirs) + for key in ours.keys(): + self.assertEqual(ours[key], theirs[key]) + + def run_operation(self, sessions, collection, operation): + original_collection = collection + name = camel_to_snake(operation['name']) + if name == 'run_command': + name = 'command' + elif name == 'download_by_name': + name = 'open_download_stream_by_name' + elif name == 'download': + name = 'open_download_stream' + + database = collection.database + collection = database.get_collection(collection.name) + if 'collectionOptions' in operation: + collection = collection.with_options( + **self.parse_options(operation['collectionOptions'])) + + object_name = self.get_object_name(operation) + if object_name == 'gridfsbucket': + # Only create the GridFSBucket when we need it (for the gridfs + # retryable reads tests). + obj = GridFSBucket( + database, bucket_name=collection.name, + disable_md5=True) + else: + objects = { + 'client': database.client, + 'database': database, + 'collection': collection, + 'testRunner': self + } + objects.update(sessions) + obj = objects[object_name] + + # Combine arguments with options and handle special cases. + arguments = operation.get('arguments', {}) + arguments.update(arguments.pop("options", {})) + self.parse_options(arguments) + + + cmd = getattr(obj, name) + if name != "bulk_write" and object_name == "collection": + wrapped_collection = ExplainCollection(obj) + explain_cmd = getattr(wrapped_collection, name) + + for arg_name in list(arguments): + c2s = camel_to_snake(arg_name) + # PyMongo accepts sort as list of tuples. + if arg_name == "sort": + sort_dict = arguments[arg_name] + arguments[arg_name] = list(iteritems(sort_dict)) + # Named "key" instead not fieldName. + if arg_name == "fieldName": + arguments["key"] = arguments.pop(arg_name) + # Aggregate uses "batchSize", while find uses batch_size. + elif ((arg_name == "batchSize" or arg_name == "allowDiskUse") and + name == "aggregate"): + continue + # Requires boolean returnDocument. + elif arg_name == "returnDocument": + arguments[c2s] = arguments.pop(arg_name) == "After" + elif c2s == "requests": + # Parse each request into a bulk write model. + requests = [] + for request in arguments["requests"]: + bulk_model = camel_to_upper_camel(request["name"]) + bulk_class = getattr(operations, bulk_model) + bulk_arguments = camel_to_snake_args(request["arguments"]) + requests.append(bulk_class(**dict(bulk_arguments))) + arguments["requests"] = requests + elif arg_name == "session": + arguments['session'] = sessions[arguments['session']] + elif name == 'command' and arg_name == 'command': + # Ensure the first key is the command name. + ordered_command = SON([(operation['command_name'], 1)]) + ordered_command.update(arguments['command']) + arguments['command'] = ordered_command + elif name == 'open_download_stream' and arg_name == 'id': + arguments['file_id'] = arguments.pop(arg_name) + elif name != 'find' and c2s == 'max_time_ms': + # find is the only method that accepts snake_case max_time_ms. + # All other methods take kwargs which must use the server's + # camelCase maxTimeMS. See PYTHON-1855. + arguments['maxTimeMS'] = arguments.pop('max_time_ms') + elif name == 'with_transaction' and arg_name == 'callback': + callback_ops = arguments[arg_name]['operations'] + arguments['callback'] = lambda _: self.run_operations( + sessions, original_collection, copy.deepcopy(callback_ops), + in_with_transaction=True) + elif name == 'drop_collection' and arg_name == 'collection': + arguments['name_or_collection'] = arguments.pop(arg_name) + elif name == 'create_collection' and arg_name == 'collection': + arguments['name'] = arguments.pop(arg_name) + elif name == 'create_index' and arg_name == 'keys': + arguments['keys'] = list(arguments.pop(arg_name).items()) + elif name == 'drop_index' and arg_name == 'name': + arguments['index_or_name'] = arguments.pop(arg_name) + else: + arguments[c2s] = arguments.pop(arg_name) + + result = cmd(**dict(arguments)) + + if name == "aggregate": + if arguments["pipeline"] and "$out" in arguments["pipeline"][-1]: + # Read from the primary to ensure causal consistency. + out = collection.database.get_collection( + arguments["pipeline"][-1]["$out"], + read_preference=ReadPreference.PRIMARY) + return out.find() + if name == "map_reduce": + if isinstance(result, dict) and 'results' in result: + return result['results'] + if 'download' in name: + result = Binary(result.read()) + + if isinstance(result, Cursor) or isinstance(result, CommandCursor): + return list(result) + + cmd_payload = self.command_logger.cmd_payload + if name != "bulk_write" and object_name == "collection": + explain_cmd(**dict(arguments)) + self._compare_command_dicts(wrapped_collection.last_cmd_payload, + cmd_payload) + return result + + def allowable_errors(self, op): + """Allow encryption spec to override expected error classes.""" + return (PyMongoError,) + + def run_operations(self, sessions, collection, ops, + in_with_transaction=False): + for op in ops: + expected_result = op.get('result') + if expect_error(op): + with self.assertRaises(self.allowable_errors(op), + msg=op['name']) as context: + self.run_operation(sessions, collection, op.copy()) + + if expect_error_message(expected_result): + if isinstance(context.exception, BulkWriteError): + errmsg = str(context.exception.details).lower() + else: + errmsg = str(context.exception).lower() + self.assertIn(expected_result['errorContains'].lower(), + errmsg) + if expect_error_code(expected_result): + self.assertEqual(expected_result['errorCodeName'], + context.exception.details.get('codeName')) + if expect_error_labels_contain(expected_result): + self.assertErrorLabelsContain( + context.exception, + expected_result['errorLabelsContain']) + if expect_error_labels_omit(expected_result): + self.assertErrorLabelsOmit( + context.exception, + expected_result['errorLabelsOmit']) + + # Reraise the exception if we're in the with_transaction + # callback. + if in_with_transaction: + raise context.exception + else: + result = self.run_operation(sessions, collection, op.copy()) + if 'result' in op: + if op['name'] == 'runCommand': + self.check_command_result(expected_result, result) + else: + self.check_result(expected_result, result) + + # TODO: factor with test_command_monitoring.py + def check_events(self, test, listener, session_ids): + res = listener.results + if not len(test['expectations']): + return + + # Give a nicer message when there are missing or extra events + cmds = decode_raw([event.command for event in res['started']]) + self.assertEqual( + len(res['started']), len(test['expectations']), cmds) + for i, expectation in enumerate(test['expectations']): + event_type = next(iter(expectation)) + event = res['started'][i] + + # The tests substitute 42 for any number other than 0. + if (event.command_name == 'getMore' + and event.command['getMore']): + event.command['getMore'] = Int64(42) + elif event.command_name == 'killCursors': + event.command['cursors'] = [Int64(42)] + elif event.command_name == 'update': + # TODO: remove this once PYTHON-1744 is done. + # Add upsert and multi fields back into expectations. + updates = expectation[event_type]['command']['updates'] + for update in updates: + update.setdefault('upsert', False) + update.setdefault('multi', False) + + # Replace afterClusterTime: 42 with actual afterClusterTime. + expected_cmd = expectation[event_type]['command'] + expected_read_concern = expected_cmd.get('readConcern') + if expected_read_concern is not None: + time = expected_read_concern.get('afterClusterTime') + if time == 42: + actual_time = event.command.get( + 'readConcern', {}).get('afterClusterTime') + if actual_time is not None: + expected_read_concern['afterClusterTime'] = actual_time + + recovery_token = expected_cmd.get('recoveryToken') + if recovery_token == 42: + expected_cmd['recoveryToken'] = CompareType(dict) + + # Replace lsid with a name like "session0" to match test. + if 'lsid' in event.command: + for name, lsid in session_ids.items(): + if event.command['lsid'] == lsid: + event.command['lsid'] = name + break + + for attr, expected in expectation[event_type].items(): + actual = getattr(event, attr) + expected = wrap_types(expected) + if isinstance(expected, dict): + for key, val in expected.items(): + if val is None: + if key in actual: + self.fail("Unexpected key [%s] in %r" % ( + key, actual)) + elif key not in actual: + self.fail("Expected key [%s] in %r" % ( + key, actual)) + else: + self.assertEqual(val, decode_raw(actual[key]), + "Key [%s] in %s" % (key, actual)) + else: + self.assertEqual(actual, expected) + + def maybe_skip_scenario(self, test): + if test.get('skipReason'): + raise unittest.SkipTest(test.get('skipReason')) + + def get_scenario_db_name(self, scenario_def): + """Allow subclasses to override a test's database name.""" + return scenario_def['database_name'] + + def get_scenario_coll_name(self, scenario_def): + """Allow subclasses to override a test's collection name.""" + return scenario_def['collection_name'] + + def get_outcome_coll_name(self, outcome, collection): + """Allow subclasses to override outcome collection.""" + return collection.name + + def run_test_ops(self, sessions, collection, test): + """Added to allow retryable writes spec to override a test's + operation.""" + self.run_operations(sessions, collection, test['operations']) + + def parse_client_options(self, opts): + """Allow encryption spec to override a clientOptions parsing.""" + # Convert test['clientOptions'] to dict to avoid a Jython bug using + # "**" with ScenarioDict. + return dict(opts) + + def setup_scenario(self, scenario_def): + """Allow specs to override a test's setup.""" + db_name = self.get_scenario_db_name(scenario_def) + coll_name = self.get_scenario_coll_name(scenario_def) + db = client_context.client.get_database( + db_name, write_concern=WriteConcern(w='majority')) + coll = db[coll_name] + coll.drop() + db.create_collection(coll_name) + if scenario_def['data']: + # Load data. + coll.insert_many(scenario_def['data']) + + def run_scenario(self, scenario_def, test): + self.maybe_skip_scenario(test) + listener = OvertCommandListener() + self.command_logger = CommandLogger() + # Create a new client, to avoid interference from pooled sessions. + client_options = self.parse_client_options(test['clientOptions']) + # MMAPv1 does not support retryable writes. + if (client_options.get('retryWrites') is True and + client_context.storage_engine == 'mmapv1'): + self.skipTest("MMAPv1 does not support retryWrites=True") + use_multi_mongos = test['useMultipleMongoses'] + if client_context.is_mongos and use_multi_mongos: + client = rs_client(client_context.mongos_seeds(), + event_listeners=[listener, self.command_logger], + **client_options) + else: + client = rs_client(event_listeners=[listener, self.command_logger], + **client_options) + self.listener = listener + # Close the client explicitly to avoid having too many threads open. + self.addCleanup(client.close) + + # Kill all sessions before and after each test to prevent an open + # transaction (from a test failure) from blocking collection/database + # operations during test set up and tear down. + self.kill_all_sessions() + self.addCleanup(self.kill_all_sessions) + + database_name = self.get_scenario_db_name(scenario_def) + collection_name = self.get_scenario_coll_name(scenario_def) + self.setup_scenario(scenario_def) + + # SPEC-1245 workaround StaleDbVersion on distinct + for c in self.mongos_clients: + c[database_name][collection_name].distinct("x") + + # Create session0 and session1. + sessions = {} + session_ids = {} + for i in range(2): + # Don't attempt to create sessions if they are not supported by + # the running server version. + if not client_context.sessions_enabled: + break + session_name = 'session%d' % i + opts = camel_to_snake_args(test['sessionOptions'][session_name]) + if 'default_transaction_options' in opts: + txn_opts = self.parse_options( + opts['default_transaction_options']) + txn_opts = client_session.TransactionOptions(**txn_opts) + opts['default_transaction_options'] = txn_opts + + s = client.start_session(**dict(opts)) + + sessions[session_name] = s + # Store lsid so we can access it after end_session, in check_events. + session_ids[session_name] = s.session_id + + self.addCleanup(end_sessions, sessions) + + if 'failPoint' in test: + fp = test['failPoint'] + self.set_fail_point(fp) + self.addCleanup(self.set_fail_point, { + 'configureFailPoint': fp['configureFailPoint'], 'mode': 'off'}) + + listener.results.clear() + + collection = client[database_name][collection_name] + self.run_test_ops(sessions, collection, test) + + end_sessions(sessions) + + self.check_events(test, listener, session_ids) + + # Disable fail points. + if 'failPoint' in test: + fp = test['failPoint'] + self.set_fail_point({ + 'configureFailPoint': fp['configureFailPoint'], 'mode': 'off'}) + + # Assert final state is expected. + outcome = test['outcome'] + expected_c = outcome.get('collection') + if expected_c is not None: + outcome_coll_name = self.get_outcome_coll_name( + outcome, collection) + + # Read from the primary with local read concern to ensure causal + # consistency. + outcome_coll = client_context.client[ + collection.database.name].get_collection( + outcome_coll_name, + read_preference=ReadPreference.PRIMARY, + read_concern=ReadConcern('local')) + actual_data = list(outcome_coll.find(sort=[('_id', 1)])) + + # The expected data needs to be the left hand side here otherwise + # CompareType(Binary) doesn't work. + self.assertEqual(wrap_types(expected_c['data']), actual_data) + +def expect_any_error(op): + if isinstance(op, dict): + return op.get('error') + + return False + + +def expect_error_message(expected_result): + if isinstance(expected_result, dict): + return isinstance(expected_result['errorContains'], text_type) + + return False + + +def expect_error_code(expected_result): + if isinstance(expected_result, dict): + return expected_result['errorCodeName'] + + return False + + +def expect_error_labels_contain(expected_result): + if isinstance(expected_result, dict): + return expected_result['errorLabelsContain'] + + return False + + +def expect_error_labels_omit(expected_result): + if isinstance(expected_result, dict): + return expected_result['errorLabelsOmit'] + + return False + + +def expect_error(op): + expected_result = op.get('result') + return (expect_any_error(op) or + expect_error_message(expected_result) + or expect_error_code(expected_result) + or expect_error_labels_contain(expected_result) + or expect_error_labels_omit(expected_result)) + + +def end_sessions(sessions): + for s in sessions.values(): + # Aborts the transaction if it's open. + s.end_session() + + +OPTS = CodecOptions(document_class=dict, uuid_representation=STANDARD) + + +def decode_raw(val): + """Decode RawBSONDocuments in the given container.""" + if isinstance(val, (list, abc.Mapping)): + return decode(encode({'v': val}, codec_options=OPTS), OPTS)['v'] + return val + + +TYPES = { + 'binData': Binary, + 'long': Int64, +} + + +def wrap_types(val): + """Support $$type assertion in command results.""" + if isinstance(val, list): + return [wrap_types(v) for v in val] + if isinstance(val, abc.Mapping): + typ = val.get('$$type') + if typ: + return CompareType(TYPES[typ]) + d = {} + for key in val: + d[key] = wrap_types(val[key]) + return d + return val diff --git a/test/version.py b/test/version.py new file mode 100644 index 0000000..3348060 --- /dev/null +++ b/test/version.py @@ -0,0 +1,88 @@ +# Copyright 2009-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Some tools for running tests based on MongoDB server version.""" + + +class Version(tuple): + + def __new__(cls, *version): + padded_version = cls._padded(version, 4) + return super(Version, cls).__new__(cls, tuple(padded_version)) + + @classmethod + def _padded(cls, iter, length, padding=0): + l = list(iter) + if len(l) < length: + for _ in range(length - len(l)): + l.append(padding) + return l + + @classmethod + def from_string(cls, version_string): + mod = 0 + bump_patch_level = False + if version_string.endswith("+"): + version_string = version_string[0:-1] + mod = 1 + elif version_string.endswith("-pre-"): + version_string = version_string[0:-5] + mod = -1 + elif version_string.endswith("-"): + version_string = version_string[0:-1] + mod = -1 + # Deal with '-rcX' substrings + if '-rc' in version_string: + version_string = version_string[0:version_string.find('-rc')] + mod = -1 + # Deal with git describe generated substrings + elif '-' in version_string: + version_string = version_string[0:version_string.find('-')] + mod = -1 + bump_patch_level = True + + + version = [int(part) for part in version_string.split(".")] + version = cls._padded(version, 3) + # Make from_string and from_version_array agree. For example: + # MongoDB Enterprise > db.runCommand('buildInfo').versionArray + # [ 3, 2, 1, -100 ] + # MongoDB Enterprise > db.runCommand('buildInfo').version + # 3.2.0-97-g1ef94fe + if bump_patch_level: + version[-1] += 1 + version.append(mod) + + return Version(*version) + + @classmethod + def from_version_array(cls, version_array): + version = list(version_array) + if version[-1] < 0: + version[-1] = -1 + version = cls._padded(version, 3) + return Version(*version) + + @classmethod + def from_client(cls, client): + info = client.server_info() + if 'versionArray' in info: + return cls.from_version_array(info['versionArray']) + return cls.from_string(info['version']) + + def at_least(self, *other_version): + return self >= Version(*other_version) + + def __str__(self): + return ".".join(map(str, self))