PYTHON-2453 Add MongoDB Versioned API (#536)

Add pymongo.server_api.ServerApi and the MongoClient server_api option.
Support Unified Test Format version 1.1 (serverParameters in
runOnRequirements)
Skip dropRole tests due to SERVER-53499.
This commit is contained in:
Shane Harvey 2021-01-11 18:16:00 -08:00 committed by GitHub
parent c96c5a9453
commit ac2f506ba2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 3047 additions and 9 deletions

View File

@ -290,6 +290,7 @@ functions:
STORAGE_ENGINE=${STORAGE_ENGINE} \
DISABLE_TEST_COMMANDS=${DISABLE_TEST_COMMANDS} \
ORCHESTRATION_FILE=${ORCHESTRATION_FILE} \
REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \
sh ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh
# run-orchestration generates expansion file with the MONGODB_URI for the cluster
- command: expansions.update
@ -425,6 +426,7 @@ functions:
AUTH=${AUTH} \
SSL=${SSL} \
DATA_LAKE=${DATA_LAKE} \
MONGODB_API_VERSION=${MONGODB_API_VERSION} \
sh ${PROJECT_DIRECTORY}/.evergreen/run-tests.sh
"run enterprise auth tests":
@ -2023,6 +2025,19 @@ axes:
variables:
SETDEFAULTENCODING: "cp1251"
- id: requireApiVersion
display_name: "requireApiVersion"
values:
- id: "requireApiVersion1"
display_name: "requireApiVersion1"
tags: [ "requireApiVersion_tag" ]
variables:
# REQUIRE_API_VERSION is set to make drivers-evergreen-tools
# start a cluster with the requireApiVersion parameter.
REQUIRE_API_VERSION: "1"
# MONGODB_API_VERSION is the apiVersion to use in the test suite.
MONGODB_API_VERSION: "1"
buildvariants:
- matrix_name: "tests-all"
matrix_spec:
@ -2605,6 +2620,17 @@ buildvariants:
tasks:
- name: atlas-data-lake-tests
- matrix_name: "versioned-api-tests"
matrix_spec:
platform: ubuntu-16.04
python-version: ["2.7", "3.9"]
auth: "auth"
requireApiVersion: "*"
display_name: "requireApiVersion ${python-version}"
tasks:
# Versioned API was introduced in MongoDB 4.7
- "test-latest-standalone"
- matrix_name: "ocsp-test"
matrix_spec:
platform: ubuntu-16.04

View File

@ -26,6 +26,7 @@ GREEN_FRAMEWORK=${GREEN_FRAMEWORK:-}
C_EXTENSIONS=${C_EXTENSIONS:-}
COVERAGE=${COVERAGE:-}
COMPRESSORS=${COMPRESSORS:-}
MONGODB_API_VERSION=${MONGODB_API_VERSION:-}
TEST_ENCRYPTION=${TEST_ENCRYPTION:-}
LIBMONGOCRYPT_URL=${LIBMONGOCRYPT_URL:-}
SETDEFAULTENCODING=${SETDEFAULTENCODING:-}
@ -35,6 +36,11 @@ if [ -n "$COMPRESSORS" ]; then
export COMPRESSORS=$COMPRESSORS
fi
if [ -n "$MONGODB_API_VERSION" ]; then
export MONGODB_API_VERSION=$MONGODB_API_VERSION
fi
export JAVA_HOME=/opt/java/jdk8
if [ "$AUTH" != "noauth" ]; then

View File

@ -54,6 +54,7 @@ Sub-modules:
read_preferences
results
son_manipulator
server_api
uri_parser
write_concern
event_loggers

View File

@ -0,0 +1,11 @@
:mod:`server_api` -- Support for MongoDB Versioned API
======================================================
.. automodule:: pymongo.server_api
:synopsis: Support for MongoDB Versioned API
.. autoclass:: pymongo.server_api.ServerApi
:members:
.. autoclass:: pymongo.server_api.ServerApiVersion
:members:

View File

@ -17,6 +17,11 @@ Breaking Changes in 4.0
- Removed support for Python 2.7, 3.4, and 3.5. Python 3.6+ is now required.
- Removed :mod:`~pymongo.thread_util`.
Notable improvements
....................
- Support for MongoDB Versioned API, see :class:`~pymongo.server_api.ServerApi`.
Issues Resolved
...............

View File

@ -125,6 +125,7 @@ def _parse_pool_options(options):
event_listeners = options.get('event_listeners')
appname = options.get('appname')
driver = options.get('driver')
server_api = options.get('server_api')
compression_settings = CompressionSettings(
options.get('compressors', []),
options.get('zlibcompressionlevel', -1))
@ -138,7 +139,8 @@ def _parse_pool_options(options):
_EventListeners(event_listeners),
appname,
driver,
compression_settings)
compression_settings,
server_api=server_api)
class ClientOptions(object):

View File

@ -297,6 +297,9 @@ class _Transaction(object):
def active(self):
return self.state in (_TxnState.STARTING, _TxnState.IN_PROGRESS)
def starting(self):
return self.state == _TxnState.STARTING
def reset(self):
self.state = _TxnState.NONE
self.sharded = False
@ -762,6 +765,12 @@ class ClientSession(object):
"""
return self._transaction.active()
@property
def _starting_transaction(self):
"""True if this session is starting a multi-statement transaction.
"""
return self._transaction.starting()
@property
def _pinned_address(self):
"""The mongos address this transaction was created on."""

View File

@ -27,6 +27,7 @@ from pymongo.auth import MECHANISMS
from pymongo.compression_support import (validate_compressors,
validate_zlib_compression_level)
from pymongo.driver_info import DriverInfo
from pymongo.server_api import ServerApi
from pymongo.encryption_options import validate_auto_encryption_opts_or_none
from pymongo.errors import ConfigurationError
from pymongo.monitoring import _validate_event_listeners
@ -528,6 +529,15 @@ def validate_driver_or_none(option, value):
return value
def validate_server_api_or_none(option, value):
"""Validate the server_api keyword arg."""
if value is None:
return value
if not isinstance(value, ServerApi):
raise TypeError("%s must be an instance of ServerApi" % (option,))
return value
def validate_is_callable_or_none(option, value):
"""Validates that 'value' is a callable."""
if value is None:
@ -643,6 +653,7 @@ URI_OPTIONS_VALIDATOR_MAP = {
NONSPEC_OPTIONS_VALIDATOR_MAP = {
'connect': validate_boolean_or_string,
'driver': validate_driver_or_none,
'server_api': validate_server_api_or_none,
'fsync': validate_boolean_or_string,
'minpoolsize': validate_non_negative_integer,
'socketkeepalive': validate_boolean_or_string,

View File

@ -704,6 +704,12 @@ class Database(common.BaseObject):
.. note:: :meth:`command` does **not** apply any custom TypeDecoders
when decoding the command response.
.. note:: If this client has been configured to use MongoDB Versioned
API (see :ref:`versioned-api-ref`), then :meth:`command` will
automactically add API versioning options to the given command.
Explicitly adding API versioning options in the command and
declaring an API version on the client is not supported.
.. versionchanged:: 3.6
Added ``session`` parameter.

View File

@ -307,6 +307,7 @@ class _Query(object):
self.name = 'explain'
cmd = SON([('explain', cmd)])
session = self.session
sock_info.add_server_api(cmd, session)
if session:
session._apply_to(cmd, False, self.read_preference)
# Explain does not support readConcern.
@ -892,6 +893,7 @@ class _BulkWriteContext(object):
self.compress = True if sock_info.compression_context else False
self.op_type = op_type
self.codec = codec
sock_info.add_server_api(command, session)
def _batch_command(self, docs):
namespace = self.db_name + '.$cmd'

View File

@ -498,8 +498,19 @@ class MongoClient(common.BaseObject):
and automatically decrypt results. See
:ref:`automatic-client-side-encryption` for an example.
| **Versioned API options:**
| (If not set explicitly, Versioned API will not be enabled.)
- `server_api`: A
:class:`~pymongo.server_api.ServerApi` which configures this
client to use Versioned API. See :ref:`versioned-api-ref` for
details.
.. mongodoc:: connections
.. versionchanged:: 3.12
Added the ``server_api`` keyword argument.
.. versionchanged:: 3.11
Added the following keyword arguments and URI options:

View File

@ -60,6 +60,7 @@ from pymongo.monitoring import (ConnectionCheckOutFailedReason,
from pymongo.network import (command,
receive_message)
from pymongo.read_preferences import ReadPreference
from pymongo.server_api import _add_to_command
from pymongo.server_type import SERVER_TYPE
from pymongo.socket_checker import SocketChecker
# Always use our backport so we always have support for IP address matching
@ -311,7 +312,7 @@ class PoolOptions(object):
'__ssl_context', '__ssl_match_hostname', '__socket_keepalive',
'__event_listeners', '__appname', '__driver', '__metadata',
'__compression_settings', '__max_connecting',
'__pause_enabled')
'__pause_enabled', '__server_api')
def __init__(self, max_pool_size=MAX_POOL_SIZE,
min_pool_size=MIN_POOL_SIZE,
@ -321,8 +322,7 @@ class PoolOptions(object):
ssl_match_hostname=True, socket_keepalive=True,
event_listeners=None, appname=None, driver=None,
compression_settings=None, max_connecting=MAX_CONNECTING,
pause_enabled=True):
pause_enabled=True, server_api=None):
self.__max_pool_size = max_pool_size
self.__min_pool_size = min_pool_size
self.__max_idle_time_seconds = max_idle_time_seconds
@ -339,6 +339,7 @@ class PoolOptions(object):
self.__compression_settings = compression_settings
self.__max_connecting = max_connecting
self.__pause_enabled = pause_enabled
self.__server_api = server_api
self.__metadata = copy.deepcopy(_METADATA)
if appname:
self.__metadata['application'] = {'name': appname}
@ -495,6 +496,12 @@ class PoolOptions(object):
"""
return self.__metadata.copy()
@property
def server_api(self):
"""A pymongo.server_api.ServerApi or None.
"""
return self.__server_api
def _negotiate_creds(all_credentials):
"""Return one credential that needs mechanism negotiation, if any.
@ -705,6 +712,7 @@ class SocketInfo(object):
raise ConfigurationError(
'Must be connected to MongoDB 3.4+ to use a collation.')
self.add_server_api(spec, session)
if session:
session._apply_to(spec, retryable_write, read_preference)
self.send_cluster_time(spec, session, client)
@ -894,6 +902,14 @@ class SocketInfo(object):
if self.max_wire_version >= 6 and client:
client._send_cluster_time(command, session)
def add_server_api(self, command, session):
"""Add server_api parameters."""
if (session and session.in_transaction and
not session._starting_transaction):
return
if self.opts.server_api:
_add_to_command(command, self.opts.server_api)
def update_last_checkin_time(self):
self.last_checkin_time = _time()

129
pymongo/server_api.py Normal file
View File

@ -0,0 +1,129 @@
# 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.
"""Support for MongoDB Versioned API.
.. _versioned-api-ref:
MongoDB Versioned API
=====================
To configure MongoDB Versioned API, pass the ``server_api`` keyword option to
:class:`~pymongo.mongo_client.MongoClient`::
from pymongo.mongo_client import MongoClient
from pymongo.server_api import ServerApi
client = MongoClient(server_api=ServerApi('1'))
Note that Versioned API requires MongoDB >=5.0.
Strict Mode
```````````
When ``strict`` mode is configured, commands that are not supported in the
given :attr:`ServerApi.version` will fail. For example::
>>> client = MongoClient(server_api=ServerApi('1', strict=True))
>>> client.test.command('count', 'test')
Traceback (most recent call last):
...
pymongo.errors.OperationFailure: Provided apiStrict:true, but the command count is not in API Version 1, full error: {'ok': 0.0, 'errmsg': 'Provided apiStrict:true, but the command count is not in API Version 1', 'code': 323, 'codeName': 'APIStrictError'
Classes
=======
"""
class ServerApiVersion:
"""An enum that defines values for :attr:`ServerApi.version`.
.. versionadded:: 3.12
"""
V1 = "1"
"""Server API version "1"."""
class ServerApi(object):
"""MongoDB Versioned API."""
def __init__(self, version, strict=None, deprecation_errors=None):
"""Options to configure MongoDB Versioned API.
:Parameters:
- `version`: The API version string. Must be one of the values in
:class:`ServerApiVersion`.
- `strict` (optional): Set to ``True`` to enable API strict mode.
Defaults to ``None`` which means "use the server's default".
- `deprecation_errors` (optional): Set to ``True`` to enable
deprecation errors. Defaults to ``None`` which means "use the
server's default".
.. versionadded:: 3.12
"""
if version != ServerApiVersion.V1:
raise ValueError("Unknown ServerApi version: %s" % (version,))
if strict is not None and not isinstance(strict, bool):
raise TypeError(
"Wrong type for ServerApi strict, value must be an instance "
"of bool, not %s" % (type(strict),))
if (deprecation_errors is not None and
not isinstance(deprecation_errors, bool)):
raise TypeError(
"Wrong type for ServerApi deprecation_errors, value must be "
"an instance of bool, not %s" % (type(deprecation_errors),))
self._version = version
self._strict = strict
self._deprecation_errors = deprecation_errors
@property
def version(self):
"""The API version setting.
This value is sent to the server in the "apiVersion" field.
"""
return self._version
@property
def strict(self):
"""The API strict mode setting.
When set, this value is sent to the server in the "apiStrict" field.
"""
return self._strict
@property
def deprecation_errors(self):
"""The API deprecation errors setting.
When set, this value is sent to the server in the
"apiDeprecationErrors" field.
"""
return self._deprecation_errors
def _add_to_command(cmd, server_api):
"""Internal helper which adds API versioning options to a command.
:Parameters:
- `cmd`: The command.
- `server_api` (optional): A :class:`ServerApi` or ``None``.
"""
if not server_api:
return
cmd['apiVersion'] = server_api.version
if server_api.strict is not None:
cmd['apiStrict'] = server_api.strict
if server_api.deprecation_errors is not None:
cmd['apiDeprecationErrors'] = server_api.deprecation_errors

View File

@ -701,6 +701,7 @@ class Topology(object):
appname=options.appname,
driver=options.driver,
pause_enabled=False,
server_api=options.server_api,
)
return self._settings.pool_class(address, monitor_pool_options,

View File

@ -48,6 +48,7 @@ import pymongo.errors
from bson.son import SON
from pymongo import common, message
from pymongo.common import partition_node
from pymongo.server_api import ServerApi
from pymongo.ssl_support import HAVE_SSL, validate_cert_reqs
from test.version import Version
@ -90,6 +91,8 @@ if CA_PEM:
TLS_OPTIONS['tlsCAFile'] = CA_PEM
COMPRESSORS = os.environ.get("COMPRESSORS")
MONGODB_API_VERSION = os.environ.get("MONGODB_API_VERSION")
def is_server_resolvable():
"""Returns True if 'server' is resolvable."""
@ -200,6 +203,7 @@ class ClientContext(object):
self.version = Version(-1) # Needs to be comparable with Version
self.auth_enabled = False
self.test_commands_enabled = False
self.server_parameters = None
self.is_mongos = False
self.mongoses = []
self.is_rs = False
@ -212,9 +216,11 @@ class ClientContext(object):
self.client = None
self.conn_lock = threading.Lock()
self.is_data_lake = False
if COMPRESSORS:
self.default_client_options["compressors"] = COMPRESSORS
if MONGODB_API_VERSION:
server_api = ServerApi(MONGODB_API_VERSION)
self.default_client_options["server_api"] = server_api
@property
def ismaster(self):
@ -226,8 +232,7 @@ class ClientContext(object):
timeout_ms = 10000
else:
timeout_ms = 5000
if COMPRESSORS:
kwargs["compressors"] = COMPRESSORS
kwargs.update(self.default_client_options)
client = pymongo.MongoClient(
host, port, serverSelectionTimeoutMS=timeout_ms, **kwargs)
try:
@ -341,6 +346,8 @@ class ClientContext(object):
self.nodes = set([(host, port)])
self.w = len(ismaster.get("hosts", [])) or 1
self.version = Version.from_client(self.client)
self.server_parameters = self.client.admin.command(
'getParameter', '*')
if 'enableTestCommands=1' in self.cmd_line['argv']:
self.test_commands_enabled = True
@ -723,6 +730,12 @@ class ClientContext(object):
"Transactions are not supported",
func=func)
def require_no_api_version(self, func):
"""Skip this test when testing with requireApiVersion."""
return self._require(lambda: not MONGODB_API_VERSION,
"This test does not work with requireApiVersion",
func=func)
def mongos_seeds(self):
return ','.join('%s:%s' % address for address in self.mongoses)
@ -766,6 +779,9 @@ def sanitize_cmd(cmd):
cp.pop('$db', None)
cp.pop('$readPreference', None)
cp.pop('lsid', None)
if MONGODB_API_VERSION:
# Versioned api parameters
cp.pop('apiVersion', 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))

View File

@ -364,6 +364,7 @@ class BulkAuthorizationTestBase(BulkTestBase):
@classmethod
@client_context.require_auth
@client_context.require_no_api_version
def setUpClass(cls):
super(BulkAuthorizationTestBase, cls).setUpClass()

View File

@ -176,6 +176,7 @@ class _TestPoolingBase(unittest.TestCase):
pool_options = client_context.client._topology_settings.pool_options
kwargs['ssl_context'] = pool_options.ssl_context
kwargs['ssl_match_hostname'] = pool_options.ssl_match_hostname
kwargs['server_api'] = pool_options.server_api
pool = Pool(pair, PoolOptions(*args, **kwargs))
pool.ready()
return pool

127
test/test_versioned_api.py Normal file
View File

@ -0,0 +1,127 @@
# 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.
import os
import sys
sys.path[0:0] = [""]
from pymongo.mongo_client import MongoClient
from pymongo.server_api import ServerApi, ServerApiVersion
from test import client_context, IntegrationTest, unittest
from test.unified_format import generate_test_classes
from test.utils import OvertCommandListener, rs_or_single_client
TEST_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'versioned-api')
# Generate unified tests.
globals().update(generate_test_classes(TEST_PATH, module=__name__))
class TestServerApi(IntegrationTest):
def test_server_api_defaults(self):
api = ServerApi(ServerApiVersion.V1)
self.assertEqual(api.version, '1')
self.assertIsNone(api.strict)
self.assertIsNone(api.deprecation_errors)
def test_server_api_explicit_false(self):
api = ServerApi('1', strict=False, deprecation_errors=False)
self.assertEqual(api.version, '1')
self.assertFalse(api.strict)
self.assertFalse(api.deprecation_errors)
def test_server_api_strict(self):
api = ServerApi('1', strict=True, deprecation_errors=True)
self.assertEqual(api.version, '1')
self.assertTrue(api.strict)
self.assertTrue(api.deprecation_errors)
def test_server_api_validation(self):
with self.assertRaises(ValueError):
ServerApi('2')
with self.assertRaises(TypeError):
ServerApi('1', strict='not-a-bool')
with self.assertRaises(TypeError):
ServerApi('1', deprecation_errors='not-a-bool')
with self.assertRaises(TypeError):
MongoClient(server_api='not-a-ServerApi')
def assertServerApi(self, event):
self.assertIn('apiVersion', event.command)
self.assertEqual(event.command['apiVersion'], '1')
def assertNoServerApi(self, event):
self.assertNotIn('apiVersion', event.command)
def assertServerApiOnlyInFirstCommand(self, events):
self.assertServerApi(events[0])
for event in events[1:]:
self.assertNoServerApi(event)
@client_context.require_version_min(4, 7)
def test_command_options(self):
listener = OvertCommandListener()
client = rs_or_single_client(server_api=ServerApi('1'),
event_listeners=[listener])
self.addCleanup(client.close)
coll = client.test.test
coll.insert_many([{} for _ in range(100)])
self.addCleanup(coll.delete_many, {})
list(coll.find(batch_size=25))
client.admin.command('ping')
for event in listener.results['started']:
if event.command_name == 'getMore':
self.assertNoServerApi(event)
else:
self.assertServerApi(event)
@client_context.require_version_min(4, 7)
@client_context.require_transactions
def test_command_options_txn(self):
listener = OvertCommandListener()
client = rs_or_single_client(server_api=ServerApi('1'),
event_listeners=[listener])
self.addCleanup(client.close)
coll = client.test.test
coll.insert_many([{} for _ in range(100)])
self.addCleanup(coll.delete_many, {})
listener.reset()
with client.start_session() as s, s.start_transaction():
coll.insert_many([{} for _ in range(100)], session=s)
list(coll.find(batch_size=25, session=s))
client.test.command('find', 'test', session=s)
self.assertServerApiOnlyInFirstCommand(listener.results['started'])
listener.reset()
with client.start_session() as s, s.start_transaction():
list(coll.find(batch_size=25, session=s))
coll.insert_many([{} for _ in range(100)], session=s)
client.test.command('find', 'test', session=s)
self.assertServerApiOnlyInFirstCommand(listener.results['started'])
listener.reset()
with client.start_session() as s, s.start_transaction():
client.test.command('find', 'test', session=s)
list(coll.find(batch_size=25, session=s))
coll.insert_many([{} for _ in range(100)], session=s)
self.assertServerApiOnlyInFirstCommand(listener.results['started'])
if __name__ == "__main__":
unittest.main()

View File

@ -45,6 +45,7 @@ from pymongo.monitoring import (
from pymongo.read_concern import ReadConcern
from pymongo.read_preferences import ReadPreference
from pymongo.results import BulkWriteResult
from pymongo.server_api import ServerApi
from pymongo.write_concern import WriteConcern
from test import client_context, unittest, IntegrationTest
@ -106,8 +107,17 @@ def is_run_on_requirement_satisfied(requirement):
max_version_satisfied = Version.from_string(
req_max_server_version) >= client_context.version
params_satisfied = True
params = requirement.get('serverParameters')
if params:
for param, val in params.items():
if param not in client_context.server_parameters:
params_satisfied = False
elif client_context.server_parameters[param] != val:
params_satisfied = False
return (topology_satisfied and min_version_satisfied and
max_version_satisfied)
max_version_satisfied and params_satisfied)
def parse_collection_or_database_options(options):
@ -200,6 +210,11 @@ class EntityMapUtil(object):
if client_context.is_mongos and spec.get('useMultipleMongoses'):
kwargs['h'] = client_context.mongos_seeds()
kwargs.update(spec.get('uriOptions', {}))
server_api = spec.get('serverApi')
if server_api:
kwargs['server_api'] = ServerApi(
server_api['version'], strict=server_api.get('strict'),
deprecation_errors=server_api.get('deprecationErrors'))
client = rs_or_single_client(**kwargs)
self[spec['id']] = client
self._test_class.addCleanup(client.close)
@ -478,6 +493,12 @@ class MatchEvaluatorUtil(object):
command = spec.get('command')
database_name = spec.get('databaseName')
if command:
if actual.command_name == 'update':
# TODO: remove this once PYTHON-1744 is done.
# Add upsert and multi fields back into expectations.
for update in command['updates']:
update.setdefault('upsert', False)
update.setdefault('multi', False)
self.match_result(command, actual.command)
if database_name:
self._test_class.assertEqual(
@ -503,7 +524,7 @@ class UnifiedSpecTestMixinV1(IntegrationTest):
Specification of the test suite being currently run is available as
a class attribute ``TEST_SPEC``.
"""
SCHEMA_VERSION = Version.from_string('1.0')
SCHEMA_VERSION = Version.from_string('1.1')
@staticmethod
def should_run_on(run_on_spec):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,117 @@
{
"description": "RunCommand helper: No API version declared",
"schemaVersion": "1.1",
"runOnRequirements": [
{
"minServerVersion": "4.7",
"serverParameters": {
"requireApiVersion": false
}
}
],
"createEntities": [
{
"client": {
"id": "client",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database",
"client": "client",
"databaseName": "versioned-api-tests"
}
}
],
"tests": [
{
"description": "runCommand does not inspect or change the command document",
"operations": [
{
"name": "runCommand",
"object": "database",
"arguments": {
"commandName": "ping",
"command": {
"ping": 1,
"apiVersion": "server_will_never_support_this_api_version"
}
},
"expectError": {
"isError": true,
"isClientError": false
}
}
],
"expectEvents": [
{
"client": "client",
"events": [
{
"commandStartedEvent": {
"command": {
"ping": 1,
"apiVersion": "server_will_never_support_this_api_version",
"apiStrict": {
"$$exists": false
},
"apiDeprecationErrors": {
"$$exists": false
}
},
"commandName": "ping",
"databaseName": "versioned-api-tests"
}
}
]
}
]
},
{
"description": "runCommand does not prevent sending invalid API version declarations",
"operations": [
{
"name": "runCommand",
"object": "database",
"arguments": {
"commandName": "ping",
"command": {
"ping": 1,
"apiStrict": true
}
},
"expectError": {
"isError": true,
"isClientError": false
}
}
],
"expectEvents": [
{
"client": "client",
"events": [
{
"commandStartedEvent": {
"command": {
"ping": 1,
"apiVersion": {
"$$exists": false
},
"apiStrict": true,
"apiDeprecationErrors": {
"$$exists": false
}
},
"commandName": "ping",
"databaseName": "versioned-api-tests"
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,73 @@
{
"description": "Test commands: deprecation errors",
"schemaVersion": "1.1",
"runOnRequirements": [
{
"minServerVersion": "4.7",
"serverParameters": {
"enableTestCommands": true,
"acceptAPIVersion2": true
}
}
],
"createEntities": [
{
"client": {
"id": "client",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database",
"client": "client",
"databaseName": "versioned-api-tests"
}
}
],
"tests": [
{
"description": "Running a command that is deprecated raises a deprecation error",
"operations": [
{
"name": "runCommand",
"object": "database",
"arguments": {
"commandName": "testDeprecationInVersion2",
"command": {
"testDeprecationInVersion2": 1,
"apiVersion": "2",
"apiDeprecationErrors": true
}
},
"expectError": {
"isError": true,
"errorContains": "command testDeprecationInVersion2 is deprecated in API Version 2",
"errorCodeName": "APIDeprecationError"
}
}
],
"expectEvents": [
{
"client": "client",
"events": [
{
"commandStartedEvent": {
"command": {
"testDeprecationInVersion2": 1,
"apiVersion": "2",
"apiStrict": {
"$$exists": false
},
"apiDeprecationErrors": true
}
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,74 @@
{
"description": "Test commands: strict mode",
"schemaVersion": "1.1",
"runOnRequirements": [
{
"minServerVersion": "4.7",
"serverParameters": {
"enableTestCommands": true
}
}
],
"createEntities": [
{
"client": {
"id": "client",
"observeEvents": [
"commandStartedEvent"
],
"serverApi": {
"version": "1",
"strict": true
}
}
},
{
"database": {
"id": "database",
"client": "client",
"databaseName": "versioned-api-tests"
}
}
],
"tests": [
{
"description": "Running a command that is not part of the versioned API results in an error",
"operations": [
{
"name": "runCommand",
"object": "database",
"arguments": {
"commandName": "testVersion2",
"command": {
"testVersion2": 1
}
},
"expectError": {
"isError": true,
"errorContains": "command testVersion2 is not in API Version 1",
"errorCodeName": "APIStrictError"
}
}
],
"expectEvents": [
{
"client": "client",
"events": [
{
"commandStartedEvent": {
"command": {
"testVersion2": 1,
"apiVersion": "1",
"apiStrict": true,
"apiDeprecationErrors": {
"$$unsetOrMatches": false
}
}
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,232 @@
{
"description": "Transaction handling",
"schemaVersion": "1.1",
"runOnRequirements": [
{
"minServerVersion": "4.7",
"topologies": [
"replicaset",
"sharded-replicaset"
]
}
],
"createEntities": [
{
"client": {
"id": "client",
"observeEvents": [
"commandStartedEvent"
],
"serverApi": {
"version": "1"
}
}
},
{
"database": {
"id": "database",
"client": "client",
"databaseName": "versioned-api-tests"
}
},
{
"collection": {
"id": "collection",
"database": "database",
"collectionName": "test"
}
},
{
"session": {
"id": "session",
"client": "client"
}
}
],
"_yamlAnchors": {
"versions": [
{
"apiVersion": "1",
"apiStrict": {
"$$unsetOrMatches": false
},
"apiDeprecationErrors": {
"$$unsetOrMatches": false
}
},
{
"apiVersion": {
"$$exists": false
},
"apiStrict": {
"$$exists": false
},
"apiDeprecationErrors": {
"$$exists": false
}
}
]
},
"initialData": [
{
"collectionName": "test",
"databaseName": "versioned-api-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
},
{
"_id": 4,
"x": 44
},
{
"_id": 5,
"x": 55
}
]
}
],
"tests": [
{
"description": "Only the first command in a transaction declares an API version",
"runOnRequirements": [
{
"topologies": [
"replicaset",
"sharded-replicaset"
]
}
],
"operations": [
{
"name": "startTransaction",
"object": "session"
},
{
"name": "insertOne",
"object": "collection",
"arguments": {
"session": "session",
"document": {
"_id": 6,
"x": 66
}
},
"expectResult": {
"$$unsetOrMatches": {
"insertedId": {
"$$unsetOrMatches": 6
}
}
}
},
{
"name": "insertOne",
"object": "collection",
"arguments": {
"session": "session",
"document": {
"_id": 7,
"x": 77
}
},
"expectResult": {
"$$unsetOrMatches": {
"insertedId": {
"$$unsetOrMatches": 7
}
}
}
},
{
"name": "commitTransaction",
"object": "session"
}
],
"expectEvents": [
{
"client": "client",
"events": [
{
"commandStartedEvent": {
"command": {
"insert": "test",
"documents": [
{
"_id": 6,
"x": 66
}
],
"lsid": {
"$$sessionLsid": "session"
},
"startTransaction": true,
"apiVersion": "1",
"apiStrict": {
"$$unsetOrMatches": false
},
"apiDeprecationErrors": {
"$$unsetOrMatches": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"insert": "test",
"documents": [
{
"_id": 7,
"x": 77
}
],
"lsid": {
"$$sessionLsid": "session"
},
"apiVersion": {
"$$exists": false
},
"apiStrict": {
"$$exists": false
},
"apiDeprecationErrors": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"commitTransaction": 1,
"lsid": {
"$$sessionLsid": "session"
},
"apiVersion": {
"$$exists": false
},
"apiStrict": {
"$$exists": false
},
"apiDeprecationErrors": {
"$$exists": false
}
}
}
}
]
}
]
}
]
}