diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 70eb6c52b..66b551de9 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -415,6 +415,12 @@ functions: export SINGLE_MONGOS_LB_URI="${SINGLE_MONGOS_LB_URI}" export MULTI_MONGOS_LB_URI="${MULTI_MONGOS_LB_URI}" fi + if [ -n "${test_serverless}" ]; then + export TEST_SERVERLESS=1 + export MONGODB_URI="${MONGODB_URI}" + export SERVERLESS_ATLAS_USER="${SERVERLESS_ATLAS_USER}" + export SERVERLESS_ATLAS_PASSWORD="${SERVERLESS_ATLAS_PASSWORD}" + fi PYTHON_BINARY=${PYTHON_BINARY} \ GREEN_FRAMEWORK=${GREEN_FRAMEWORK} \ @@ -836,9 +842,41 @@ post: - func: "cleanup" - func: "teardown_docker" +task_groups: + - name: serverless_task_group + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 # 30 minutes + setup_group: + - func: "fetch source" + - func: "prepare resources" + - command: shell.exec + params: + shell: "bash" + script: | + ${PREPARE_SHELL} + set +o xtrace + SERVERLESS_DRIVERS_GROUP=${SERVERLESS_DRIVERS_GROUP} \ + SERVERLESS_API_PUBLIC_KEY=${SERVERLESS_API_PUBLIC_KEY} \ + SERVERLESS_API_PRIVATE_KEY=${SERVERLESS_API_PRIVATE_KEY} \ + bash ${DRIVERS_TOOLS}/.evergreen/serverless/create-instance.sh + - command: expansions.update + params: + file: serverless-expansion.yml + teardown_group: + - command: shell.exec + params: + script: | + ${PREPARE_SHELL} + set +o xtrace + SERVERLESS_DRIVERS_GROUP=${SERVERLESS_DRIVERS_GROUP} \ + SERVERLESS_API_PUBLIC_KEY=${SERVERLESS_API_PUBLIC_KEY} \ + SERVERLESS_API_PRIVATE_KEY=${SERVERLESS_API_PRIVATE_KEY} \ + SERVERLESS_INSTANCE_NAME=${SERVERLESS_INSTANCE_NAME} \ + bash ${DRIVERS_TOOLS}/.evergreen/serverless/delete-instance.sh + tasks: + - ".serverless" + tasks: - - # Wildcard task. Do you need to find out what tools are available and where? # Throw it here, and execute this task on all buildvariants - name: getdata @@ -1184,6 +1222,11 @@ tasks: TOPOLOGY: "sharded_cluster" - func: "run tests" + - name: "test-serverless" + tags: ["serverless"] + commands: + - func: "run tests" + - name: "test-enterprise-auth" tags: ["enterprise-auth"] commands: @@ -2040,6 +2083,15 @@ axes: test_loadbalancer: true batchtime: 10080 # 7 days + - id: serverless + display_name: "Serverless" + values: + - id: "enabled" + display_name: "Serverless" + variables: + test_serverless: true + batchtime: 10080 # 7 days + buildvariants: - matrix_name: "tests-all" matrix_spec: @@ -2473,6 +2525,16 @@ buildvariants: tasks: - name: "atlas-connect" +- matrix_name: "serverless" + matrix_spec: + platform: awslinux + python-version: *amazon1-pythons + auth-ssl: auth-ssl + serverless: "*" + display_name: "Serverless ${python-version} ${platform}" + tasks: + - "serverless_task_group" + - matrix_name: "data-lake-spec-tests" matrix_spec: platform: ubuntu-16.04 diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index d45e9d523..ff6c54ac2 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -39,12 +39,15 @@ if [ -n "$MONGODB_API_VERSION" ]; then fi if [ "$AUTH" != "noauth" ]; then - if [ -z "$DATA_LAKE" ]; then - export DB_USER="bob" - export DB_PASSWORD="pwd123" - else + if [ ! -z "$DATA_LAKE" ]; then export DB_USER="mhuser" export DB_PASSWORD="pencil" + elif [ ! -z "$TEST_SERVERLESS" ]; then + export DB_USER=$SERVERLESS_ATLAS_USER + export DB_PASSWORD=$SERVERLESS_ATLAS_PASSWORD + else + export DB_USER="bob" + export DB_PASSWORD="pwd123" fi fi diff --git a/test/__init__.py b/test/__init__.py index 49d20ccfb..54761fb55 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -94,6 +94,7 @@ if CA_PEM: COMPRESSORS = os.environ.get("COMPRESSORS") MONGODB_API_VERSION = os.environ.get("MONGODB_API_VERSION") TEST_LOADBALANCER = bool(os.environ.get("TEST_LOADBALANCER")) +TEST_SERVERLESS = bool(os.environ.get("TEST_SERVERLESS")) SINGLE_MONGOS_LB_URI = os.environ.get("SINGLE_MONGOS_LB_URI") MULTI_MONGOS_LB_URI = os.environ.get("MULTI_MONGOS_LB_URI") if TEST_LOADBALANCER: @@ -104,6 +105,13 @@ if TEST_LOADBALANCER: host, port = res['nodelist'][0] db_user = res['username'] or db_user db_pwd = res['password'] or db_pwd +elif TEST_SERVERLESS: + res = parse_uri(os.environ["MONGODB_URI"]) + host, port = res['nodelist'].pop(0) + additional_serverless_mongoses = res['nodelist'] + db_user = res['username'] or db_user + db_pwd = res['password'] or db_pwd + TLS_OPTIONS = {'tls': True} def is_server_resolvable(): @@ -231,6 +239,7 @@ class ClientContext(object): self.conn_lock = threading.Lock() self.is_data_lake = False self.load_balancer = TEST_LOADBALANCER + self.serverless = TEST_SERVERLESS if self.load_balancer: self.default_client_options["loadBalanced"] = True if COMPRESSORS: @@ -309,22 +318,26 @@ class ClientContext(object): 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 + if self.serverless: + self.auth_enabled = True else: - self.auth_enabled = self._server_started_with_auth() + 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) + if not self.serverless: + # 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, @@ -334,10 +347,13 @@ class ClientContext(object): # 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 + if self.serverless: + self.server_status = {} + else: + 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 @@ -374,33 +390,41 @@ 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']: + if TEST_SERVERLESS: self.test_commands_enabled = True - elif 'parsed' in self.cmd_line: - params = self.cmd_line['parsed'].get('setParameter', []) - if 'enableTestCommands=1' in params: + self.has_ipv6 = False + else: + self.server_parameters = self.client.admin.command( + 'getParameter', '*') + if 'enableTestCommands=1' in self.cmd_line['argv']: self.test_commands_enabled = True - else: - params = self.cmd_line['parsed'].get('setParameter', {}) - if params.get('enableTestCommands') == '1': + 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.has_ipv6 = self._server_started_with_ipv6() 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) + if self.serverless: + self.mongoses.append(self.client.address) + self.mongoses.extend(additional_serverless_mongoses) + else: + # 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: @@ -891,6 +915,9 @@ class IntegrationTest(PyMongoTestCase): if (client_context.load_balancer and not getattr(cls, 'RUN_ON_LOAD_BALANCER', False)): raise SkipTest('this test does not support load balancers') + if (client_context.serverless and + not getattr(cls, 'RUN_ON_SERVERLESS', False)): + raise SkipTest('this test does not support serverless') cls.client = client_context.client cls.db = cls.client.pymongo_test if client_context.auth_enabled: diff --git a/test/crud/unified/aggregate-let.json b/test/crud/unified/aggregate-let.json index 4ce8256cb..d3b76bd65 100644 --- a/test/crud/unified/aggregate-let.json +++ b/test/crud/unified/aggregate-let.json @@ -1,6 +1,6 @@ { "description": "aggregate-let", - "schemaVersion": "1.0", + "schemaVersion": "1.4", "createEntities": [ { "client": { @@ -310,7 +310,8 @@ "description": "Aggregate to collection with let option", "runOnRequirements": [ { - "minServerVersion": "5.0" + "minServerVersion": "5.0", + "serverless": "forbid" } ], "operations": [ diff --git a/test/crud/unified/aggregate-out-readConcern.json b/test/crud/unified/aggregate-out-readConcern.json index ae1beedde..e293457c1 100644 --- a/test/crud/unified/aggregate-out-readConcern.json +++ b/test/crud/unified/aggregate-out-readConcern.json @@ -1,13 +1,14 @@ { "description": "aggregate-out-readConcern", - "schemaVersion": "1.1", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.1.0", "topologies": [ "replicaset", "sharded" - ] + ], + "serverless": "forbid" } ], "createEntities": [ diff --git a/test/crud/v1/read/aggregate-collation.json b/test/crud/v1/read/aggregate-collation.json index 85662a442..d958e447b 100644 --- a/test/crud/v1/read/aggregate-collation.json +++ b/test/crud/v1/read/aggregate-collation.json @@ -6,6 +6,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "Aggregate with collation", diff --git a/test/crud/v1/read/aggregate-out.json b/test/crud/v1/read/aggregate-out.json index 4e33f9288..c195e163e 100644 --- a/test/crud/v1/read/aggregate-out.json +++ b/test/crud/v1/read/aggregate-out.json @@ -14,6 +14,7 @@ } ], "minServerVersion": "2.6", + "serverless": "forbid", "tests": [ { "description": "Aggregate with $out", diff --git a/test/crud/v1/read/count-collation.json b/test/crud/v1/read/count-collation.json index 6f75282fe..7d6150849 100644 --- a/test/crud/v1/read/count-collation.json +++ b/test/crud/v1/read/count-collation.json @@ -6,6 +6,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "Count documents with collation", diff --git a/test/crud/v1/read/distinct-collation.json b/test/crud/v1/read/distinct-collation.json index 0af0c67cb..984991a43 100644 --- a/test/crud/v1/read/distinct-collation.json +++ b/test/crud/v1/read/distinct-collation.json @@ -10,6 +10,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "Distinct with a collation", diff --git a/test/crud/v1/read/find-collation.json b/test/crud/v1/read/find-collation.json index 53d0e9490..4e56c0525 100644 --- a/test/crud/v1/read/find-collation.json +++ b/test/crud/v1/read/find-collation.json @@ -6,6 +6,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "Find with a collation", diff --git a/test/crud/v1/write/bulkWrite-collation.json b/test/crud/v1/write/bulkWrite-collation.json index 8e9d1bcb1..bc90aa817 100644 --- a/test/crud/v1/write/bulkWrite-collation.json +++ b/test/crud/v1/write/bulkWrite-collation.json @@ -22,6 +22,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "BulkWrite with delete operations and collation", diff --git a/test/crud/v1/write/deleteMany-collation.json b/test/crud/v1/write/deleteMany-collation.json index d17bf3bcb..fce75e488 100644 --- a/test/crud/v1/write/deleteMany-collation.json +++ b/test/crud/v1/write/deleteMany-collation.json @@ -14,6 +14,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "DeleteMany when many documents match with collation", diff --git a/test/crud/v1/write/deleteOne-collation.json b/test/crud/v1/write/deleteOne-collation.json index 2f7f92113..9bcef411e 100644 --- a/test/crud/v1/write/deleteOne-collation.json +++ b/test/crud/v1/write/deleteOne-collation.json @@ -14,6 +14,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "DeleteOne when many documents matches with collation", diff --git a/test/crud/v1/write/findOneAndDelete-collation.json b/test/crud/v1/write/findOneAndDelete-collation.json index 1ff37d2e8..32480da84 100644 --- a/test/crud/v1/write/findOneAndDelete-collation.json +++ b/test/crud/v1/write/findOneAndDelete-collation.json @@ -14,6 +14,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "FindOneAndDelete when one document matches with collation", diff --git a/test/crud/v1/write/findOneAndReplace-collation.json b/test/crud/v1/write/findOneAndReplace-collation.json index babb2f7c1..9b3c25005 100644 --- a/test/crud/v1/write/findOneAndReplace-collation.json +++ b/test/crud/v1/write/findOneAndReplace-collation.json @@ -10,6 +10,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "FindOneAndReplace when one document matches with collation returning the document after modification", diff --git a/test/crud/v1/write/findOneAndUpdate-collation.json b/test/crud/v1/write/findOneAndUpdate-collation.json index 04c1fe73e..8abab7bd6 100644 --- a/test/crud/v1/write/findOneAndUpdate-collation.json +++ b/test/crud/v1/write/findOneAndUpdate-collation.json @@ -14,6 +14,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "FindOneAndUpdate when many documents match with collation returning the document before modification", diff --git a/test/crud/v1/write/replaceOne-collation.json b/test/crud/v1/write/replaceOne-collation.json index a668fe738..fa4cbe997 100644 --- a/test/crud/v1/write/replaceOne-collation.json +++ b/test/crud/v1/write/replaceOne-collation.json @@ -10,6 +10,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "ReplaceOne when one document matches with collation", diff --git a/test/crud/v1/write/updateMany-collation.json b/test/crud/v1/write/updateMany-collation.json index 3cb49f229..8becfd806 100644 --- a/test/crud/v1/write/updateMany-collation.json +++ b/test/crud/v1/write/updateMany-collation.json @@ -14,6 +14,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "UpdateMany when many documents match with collation", diff --git a/test/crud/v1/write/updateOne-collation.json b/test/crud/v1/write/updateOne-collation.json index c49112d51..3afdb83e0 100644 --- a/test/crud/v1/write/updateOne-collation.json +++ b/test/crud/v1/write/updateOne-collation.json @@ -10,6 +10,7 @@ } ], "minServerVersion": "3.4", + "serverless": "forbid", "tests": [ { "description": "UpdateOne when one document matches with collation", diff --git a/test/retryable_reads/mapReduce.json b/test/retryable_reads/mapReduce.json index e76aa76cb..9327a2305 100644 --- a/test/retryable_reads/mapReduce.json +++ b/test/retryable_reads/mapReduce.json @@ -12,7 +12,8 @@ "topology": [ "sharded", "load-balanced" - ] + ], + "serverless": "forbid" } ], "database_name": "retryable-reads-tests", diff --git a/test/test_auth.py b/test/test_auth.py index 49081c7cc..f7b13a1cf 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -27,7 +27,7 @@ from pymongo.auth import HAVE_KERBEROS, _build_credentials_tuple from pymongo.errors import OperationFailure from pymongo.read_preferences import ReadPreference from pymongo.saslprep import HAVE_STRINGPREP -from test import client_context, SkipTest, unittest, Version +from test import client_context, IntegrationTest, SkipTest, unittest, Version from test.utils import (delay, ignore_deprecations, single_client, @@ -303,11 +303,12 @@ class TestSASLPlain(unittest.TestCase): self.assertRaises(OperationFailure, bad_pwd.admin.command, 'ismaster') -class TestSCRAMSHA1(unittest.TestCase): +class TestSCRAMSHA1(IntegrationTest): @client_context.require_auth @client_context.require_version_min(2, 7, 2) def setUp(self): + super(TestSCRAMSHA1, self).setUp() # Before 2.7.7, SCRAM-SHA-1 had to be enabled from the command line. if client_context.version < Version(2, 7, 7): cmd_line = client_context.cmd_line @@ -321,6 +322,7 @@ class TestSCRAMSHA1(unittest.TestCase): def tearDown(self): client_context.drop_user('pymongo_test', 'user') + super(TestSCRAMSHA1, self).tearDown() def test_scram_sha1(self): host, port = client_context.host, client_context.port @@ -343,11 +345,12 @@ class TestSCRAMSHA1(unittest.TestCase): # https://github.com/mongodb/specifications/blob/master/source/auth/auth.rst#scram-sha-256-and-mechanism-negotiation -class TestSCRAM(unittest.TestCase): +class TestSCRAM(IntegrationTest): @client_context.require_auth @client_context.require_version_min(3, 7, 2) def setUp(self): + super(TestSCRAM, self).setUp() self._SENSITIVE_COMMANDS = monitoring._SENSITIVE_COMMANDS monitoring._SENSITIVE_COMMANDS = set([]) self.listener = WhiteListEventListener("saslStart") @@ -356,6 +359,7 @@ class TestSCRAM(unittest.TestCase): monitoring._SENSITIVE_COMMANDS = self._SENSITIVE_COMMANDS client_context.client.testscram.command("dropAllUsersFromDatabase") client_context.client.drop_database("testscram") + super(TestSCRAM, self).tearDown() def test_scram_skip_empty_exchange(self): listener = WhiteListEventListener("saslStart", "saslContinue") @@ -571,10 +575,11 @@ class TestSCRAM(unittest.TestCase): self.assertTrue(thread.success) -class TestAuthURIOptions(unittest.TestCase): +class TestAuthURIOptions(IntegrationTest): @client_context.require_auth def setUp(self): + super(TestAuthURIOptions, self).setUp() client_context.create_user('admin', 'admin', 'pass') client_context.create_user( 'pymongo_test', 'user', 'pass', ['userAdmin', 'readWrite']) @@ -582,6 +587,7 @@ class TestAuthURIOptions(unittest.TestCase): def tearDown(self): client_context.drop_user('pymongo_test', 'user') client_context.drop_user('admin', 'admin') + super(TestAuthURIOptions, self).tearDown() def test_uri_options(self): # Test default to admin diff --git a/test/test_change_stream.py b/test/test_change_stream.py index 669a819aa..8c1bec1a6 100644 --- a/test/test_change_stream.py +++ b/test/test_change_stream.py @@ -1046,14 +1046,17 @@ class TestAllLegacyScenarios(IntegrationTest): @classmethod @client_context.require_connection def setUpClass(cls): + super(TestAllLegacyScenarios, cls).setUpClass() cls.listener = WhiteListEventListener("aggregate", "getMore") cls.client = rs_or_single_client(event_listeners=[cls.listener]) @classmethod def tearDownClass(cls): cls.client.close() + super(TestAllLegacyScenarios, cls).tearDownClass() def setUp(self): + super(TestAllLegacyScenarios, self).setUp() self.listener.results.clear() def setUpCluster(self, scenario_dict): diff --git a/test/test_client_context.py b/test/test_client_context.py index 512347daa..52a1ee92b 100644 --- a/test/test_client_context.py +++ b/test/test_client_context.py @@ -30,6 +30,15 @@ class TestClientContext(unittest.TestCase): 'PYMONGO_MUST_CONNECT is set. Failed attempts:\n%s' % (client_context.connection_attempt_info(),)) + def test_serverless(self): + if 'TEST_SERVERLESS' not in os.environ: + raise SkipTest('TEST_SERVERLESS is not set') + + self.assertTrue(client_context.connected and client_context.serverless, + 'client context must be connected to serverless when ' + 'TEST_SERVERLESS is set. Failed attempts:\n%s' % + (client_context.connection_attempt_info(),)) + def test_enableTestCommands_is_disabled(self): if 'PYMONGO_DISABLE_TEST_COMMANDS' not in os.environ: raise SkipTest('PYMONGO_DISABLE_TEST_COMMANDS is not set') diff --git a/test/test_collation.py b/test/test_collation.py index 4954af7bd..3aca63d47 100644 --- a/test/test_collation.py +++ b/test/test_collation.py @@ -25,7 +25,7 @@ from pymongo.errors import ConfigurationError from pymongo.operations import (DeleteMany, DeleteOne, IndexModel, ReplaceOne, UpdateMany, UpdateOne) from pymongo.write_concern import WriteConcern -from test import unittest, client_context +from test import client_context, IntegrationTest, unittest from test.utils import EventListener, ignore_deprecations, rs_or_single_client @@ -88,11 +88,11 @@ def raisesConfigurationErrorForOldMongoDB(func): return wrapper -class TestCollation(unittest.TestCase): - +class TestCollation(IntegrationTest): @classmethod @client_context.require_connection def setUpClass(cls): + super(TestCollation, cls).setUpClass() cls.listener = EventListener() cls.client = rs_or_single_client(event_listeners=[cls.listener]) cls.db = cls.client.pymongo_test @@ -106,9 +106,11 @@ class TestCollation(unittest.TestCase): cls.warn_context.__exit__() cls.warn_context = None cls.client.close() + super(TestCollation, cls).tearDownClass() def tearDown(self): self.listener.results.clear() + super(TestCollation, self).tearDown() def last_command_started(self): return self.listener.results['started'][-1].command diff --git a/test/test_crud_unified.py b/test/test_crud_unified.py index a2aece6ff..a435c1caa 100644 --- a/test/test_crud_unified.py +++ b/test/test_crud_unified.py @@ -28,7 +28,8 @@ TEST_PATH = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'crud', 'unified') # Generate unified tests. -globals().update(generate_test_classes(TEST_PATH, module=__name__)) +globals().update(generate_test_classes( + TEST_PATH, module=__name__, RUN_ON_SERVERLESS=True)) if __name__ == "__main__": unittest.main() diff --git a/test/test_crud_v1.py b/test/test_crud_v1.py index 6650ae29d..17f77a163 100644 --- a/test/test_crud_v1.py +++ b/test/test_crud_v1.py @@ -32,7 +32,7 @@ from pymongo.operations import (InsertOne, UpdateOne, UpdateMany) -from test import unittest, IntegrationTest +from test import client_context, unittest, IntegrationTest from test.utils import (camel_to_snake, camel_to_upper_camel, camel_to_snake_args, drop_collections, TestCreator) @@ -42,7 +42,7 @@ _TEST_PATH = os.path.join( class TestAllScenarios(IntegrationTest): - pass + RUN_ON_SERVERLESS = True def check_result(self, expected_result, result): diff --git a/test/test_monitoring.py b/test/test_monitoring.py index 464231b52..a6fe5c9df 100644 --- a/test/test_monitoring.py +++ b/test/test_monitoring.py @@ -32,7 +32,7 @@ from pymongo.read_preferences import ReadPreference from pymongo.write_concern import WriteConcern from test import (client_context, client_knobs, - PyMongoTestCase, + IntegrationTest, sanitize_cmd, unittest) from test.utils import (EventListener, @@ -42,11 +42,12 @@ from test.utils import (EventListener, wait_until) -class TestCommandMonitoring(PyMongoTestCase): +class TestCommandMonitoring(IntegrationTest): @classmethod @client_context.require_connection def setUpClass(cls): + super(TestCommandMonitoring, cls).setUpClass() cls.listener = EventListener() cls.client = rs_or_single_client( event_listeners=[cls.listener], @@ -55,9 +56,11 @@ class TestCommandMonitoring(PyMongoTestCase): @classmethod def tearDownClass(cls): cls.client.close() + super(TestCommandMonitoring, cls).tearDownClass() def tearDown(self): self.listener.results.clear() + super(TestCommandMonitoring, self).tearDown() def test_started_simple(self): self.client.pymongo_test.command('ismaster') @@ -1129,11 +1132,12 @@ class TestCommandMonitoring(PyMongoTestCase): self.assertEqual({}, succeeded.reply) -class TestGlobalListener(PyMongoTestCase): +class TestGlobalListener(IntegrationTest): @classmethod @client_context.require_connection def setUpClass(cls): + super(TestGlobalListener, cls).setUpClass() cls.listener = EventListener() # We plan to call register(), which internally modifies _LISTENERS. cls.saved_listeners = copy.deepcopy(monitoring._LISTENERS) @@ -1146,8 +1150,10 @@ class TestGlobalListener(PyMongoTestCase): def tearDownClass(cls): monitoring._LISTENERS = cls.saved_listeners cls.client.close() + super(TestGlobalListener, cls).tearDownClass() def setUp(self): + super(TestGlobalListener, self).setUp() self.listener.results.clear() def test_simple(self): @@ -1167,7 +1173,7 @@ class TestGlobalListener(PyMongoTestCase): self.assertTrue(isinstance(started.request_id, int)) -class TestEventClasses(PyMongoTestCase): +class TestEventClasses(unittest.TestCase): def test_command_event_repr(self): request_id, connection_id, operation_id = 1, ('localhost', 27017), 2 diff --git a/test/test_pooling.py b/test/test_pooling.py index 11fcb1ce7..5746f56d1 100644 --- a/test/test_pooling.py +++ b/test/test_pooling.py @@ -34,7 +34,7 @@ sys.path[0:0] = [""] from pymongo.pool import Pool, PoolOptions from pymongo.socket_checker import SocketChecker -from test import client_context, unittest +from test import client_context, IntegrationTest, unittest from test.utils import (get_pool, joinall, delay, @@ -154,10 +154,11 @@ def run_cases(client, cases): assert t.passed, "%s.run() threw an exception" % repr(t) -class _TestPoolingBase(unittest.TestCase): +class _TestPoolingBase(IntegrationTest): """Base class for all connection-pool tests.""" def setUp(self): + super(_TestPoolingBase, self).setUp() self.c = rs_or_single_client() db = self.c[DB] db.unique.drop() @@ -167,6 +168,7 @@ class _TestPoolingBase(unittest.TestCase): def tearDown(self): self.c.close() + super(_TestPoolingBase, self).tearDown() def create_pool( self, diff --git a/test/test_read_concern.py b/test/test_read_concern.py index 2eef4cb1d..0dc9b609b 100644 --- a/test/test_read_concern.py +++ b/test/test_read_concern.py @@ -15,18 +15,19 @@ """Test the read_concern module.""" from bson.son import SON -from pymongo.errors import ConfigurationError, OperationFailure +from pymongo.errors import ConfigurationError from pymongo.read_concern import ReadConcern -from test import client_context, PyMongoTestCase +from test import client_context, IntegrationTest from test.utils import single_client, rs_or_single_client, OvertCommandListener -class TestReadConcern(PyMongoTestCase): +class TestReadConcern(IntegrationTest): @classmethod @client_context.require_connection def setUpClass(cls): + super(TestReadConcern, cls).setUpClass() cls.listener = OvertCommandListener() cls.client = single_client(event_listeners=[cls.listener]) cls.db = cls.client.pymongo_test @@ -36,9 +37,11 @@ class TestReadConcern(PyMongoTestCase): def tearDownClass(cls): cls.client.close() client_context.client.pymongo_test.drop_collection('coll') + super(TestReadConcern, cls).tearDownClass() def tearDown(self): self.listener.results.clear() + super(TestReadConcern, self).tearDown() def test_read_concern(self): rc = ReadConcern() diff --git a/test/test_read_preferences.py b/test/test_read_preferences.py index d70ad7e07..6f6634234 100644 --- a/test/test_read_preferences.py +++ b/test/test_read_preferences.py @@ -49,7 +49,7 @@ from test.utils import (connected, from test.version import Version -class TestSelections(unittest.TestCase): +class TestSelections(IntegrationTest): @client_context.require_connection def test_bool(self): @@ -471,7 +471,8 @@ class TestMovingAverage(unittest.TestCase): avg.add_sample(30) self.assertAlmostEqual(15.6, avg.get()) -class TestMongosAndReadPreference(unittest.TestCase): + +class TestMongosAndReadPreference(IntegrationTest): def test_read_preference_document(self): diff --git a/test/test_retryable_reads.py b/test/test_retryable_reads.py index 381f2c1d8..1995b5dc3 100644 --- a/test/test_retryable_reads.py +++ b/test/test_retryable_reads.py @@ -52,6 +52,7 @@ class TestClientOptions(PyMongoTestCase): class TestSpec(SpecRunner): RUN_ON_LOAD_BALANCER = True + RUN_ON_SERVERLESS = True @classmethod @client_context.require_failCommand_fail_point @@ -68,11 +69,25 @@ class TestSpec(SpecRunner): if name.lower() in test['description'].lower(): self.skipTest('PyMongo does not support %s' % (name,)) - # Skip changeStream related tests on MMAPv1. + # Serverless does not support $out and collation. + if client_context.serverless: + for operation in test['operations']: + if operation['name'] == 'aggregate': + for stage in operation['arguments']['pipeline']: + if "$out" in stage: + self.skipTest( + "MongoDB Serverless does not support $out") + if "collation" in operation['arguments']: + self.skipTest( + "MongoDB Serverless does not support collations") + + # Skip changeStream related tests on MMAPv1 and serverless. test_name = self.id().rsplit('.')[-1] - if ('changestream' in test_name.lower() and - client_context.storage_engine == 'mmapv1'): - self.skipTest("MMAPv1 does not support change streams.") + if 'changestream' in test_name.lower(): + if client_context.storage_engine == 'mmapv1': + self.skipTest("MMAPv1 does not support change streams.") + if client_context.serverless: + self.skipTest("Serverless does not support change streams.") def get_scenario_coll_name(self, scenario_def): """Override a test's collection name to support GridFS tests.""" diff --git a/test/test_retryable_writes.py b/test/test_retryable_writes.py index db2e1455d..0368f97a6 100644 --- a/test/test_retryable_writes.py +++ b/test/test_retryable_writes.py @@ -55,6 +55,7 @@ _TEST_PATH = os.path.join( class TestAllScenarios(SpecRunner): RUN_ON_LOAD_BALANCER = True + RUN_ON_SERVERLESS = True def get_object_name(self, op): return op.get('object', 'collection') @@ -123,6 +124,7 @@ def non_retryable_single_statement_ops(coll): class IgnoreDeprecationsTest(IntegrationTest): RUN_ON_LOAD_BALANCER = True + RUN_ON_SERVERLESS = True @classmethod def setUpClass(cls): @@ -420,6 +422,7 @@ class TestRetryableWrites(IgnoreDeprecationsTest): class TestWriteConcernError(IntegrationTest): RUN_ON_LOAD_BALANCER = True + RUN_ON_SERVERLESS = True @classmethod @client_context.require_replica_set diff --git a/test/test_session.py b/test/test_session.py index 41837ab21..df1865c4c 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -958,17 +958,27 @@ class TestCausalConsistency(unittest.TestCase): lambda coll, session: coll.drop_index("foo_1", session=session)) self._test_no_read_concern( lambda coll, session: coll.drop_indexes(session=session)) + + # Not a write, but explain also doesn't support readConcern. + self._test_no_read_concern( + lambda coll, session: coll.find({}, session=session).explain()) + + @client_context.require_no_standalone + @unittest.skipIf(client_context.serverless, + "Serverless does not support currentOp") + def test_writes_do_not_include_read_concern_current_op(self): + # Not a write, but currentOp also doesn't support readConcern. + self._test_no_read_concern( + lambda coll, session: coll.database.current_op(session=session)) + + @client_context.require_no_standalone + @unittest.skipIf(client_context.serverless, + "Serverless does not support mapReduce") + def test_writes_do_not_include_read_concern_map_reduce(self): self._test_no_read_concern( lambda coll, session: coll.map_reduce( 'function() {}', 'function() {}', 'mrout', session=session)) - # They are not writes, but currentOp and explain also don't support - # readConcern. - self._test_no_read_concern( - lambda coll, session: coll.database.current_op(session=session)) - self._test_no_read_concern( - lambda coll, session: coll.find({}, session=session).explain()) - @client_context.require_no_standalone @client_context.require_version_max(4, 1, 0) def test_aggregate_out_does_not_include_read_concern(self): @@ -1164,6 +1174,7 @@ class TestClusterTime(IntegrationTest): class TestSpec(SpecRunner): + RUN_ON_SERVERLESS = True # Location of JSON test specifications. TEST_PATH = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'sessions', 'legacy') diff --git a/test/test_transactions.py b/test/test_transactions.py index b9c292caa..1e0318dd7 100644 --- a/test/test_transactions.py +++ b/test/test_transactions.py @@ -76,6 +76,7 @@ class TransactionsBase(SpecRunner): class TestTransactions(TransactionsBase): + RUN_ON_SERVERLESS = True @client_context.require_transactions def test_transaction_options_validation(self): default_options = TransactionOptions() diff --git a/test/test_versioned_api.py b/test/test_versioned_api.py index f092c434b..44fc89ac7 100644 --- a/test/test_versioned_api.py +++ b/test/test_versioned_api.py @@ -34,6 +34,7 @@ globals().update(generate_test_classes(TEST_PATH, module=__name__)) class TestServerApi(IntegrationTest): RUN_ON_LOAD_BALANCER = True + RUN_ON_SERVERLESS = True def test_server_api_defaults(self): api = ServerApi(ServerApiVersion.V1) diff --git a/test/unified_format.py b/test/unified_format.py index 48a1e4940..3c8c0ff04 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -117,6 +117,14 @@ def is_run_on_requirement_satisfied(requirement): max_version_satisfied = Version.from_string( req_max_server_version) >= server_version + serverless = requirement.get('serverless') + if serverless == "require": + serverless_satisfied = client_context.serverless + elif serverless == "forbid": + serverless_satisfied = not client_context.serverless + else: # unset or "allow" + serverless_satisfied = True + params_satisfied = True params = requirement.get('serverParameters') if params: @@ -135,7 +143,8 @@ def is_run_on_requirement_satisfied(requirement): auth_satisfied = not client_context.auth_enabled return (topology_satisfied and min_version_satisfied and - max_version_satisfied and params_satisfied and auth_satisfied) + max_version_satisfied and serverless_satisfied and + params_satisfied and auth_satisfied) def parse_collection_or_database_options(options): @@ -1154,7 +1163,8 @@ _SCHEMA_VERSION_MAJOR_TO_MIXIN_CLASS = { def generate_test_classes(test_path, module=__name__, class_name_prefix='', expected_failures=[], - bypass_test_generation_errors=False): + bypass_test_generation_errors=False, + **kwargs): """Method for generating test classes. Returns a dictionary where keys are the names of test classes and values are the test class objects.""" test_klasses = {} @@ -1195,10 +1205,12 @@ def generate_test_classes(test_path, module=__name__, class_name_prefix='', raise ValueError( "test file '%s' has unsupported schemaVersion '%s'" % ( fpath, schema_version)) + module_dict = {'__module__': module} + module_dict.update(kwargs) test_klasses[class_name] = type( class_name, (mixin_class, test_base_class_factory(scenario_def),), - {'__module__': module}) + module_dict) except Exception: if bypass_test_generation_errors: continue diff --git a/test/utils.py b/test/utils.py index ae75d7647..2301389c3 100644 --- a/test/utils.py +++ b/test/utils.py @@ -24,6 +24,7 @@ import shutil import sys import threading import time +import unittest import warnings from collections import abc, defaultdict @@ -391,6 +392,18 @@ class TestCreator(object): if max_ver is not None: method = client_context.require_version_max(*max_ver)(method) + if 'serverless' in scenario_def: + serverless = scenario_def['serverless'] + if serverless == "require": + serverless_satisfied = client_context.serverless + elif serverless == "forbid": + serverless_satisfied = not client_context.serverless + else: # unset or "allow" + serverless_satisfied = True + method = unittest.skipUnless( + serverless_satisfied, + "Serverless requirement not satisfied")(method) + return method @staticmethod @@ -423,6 +436,16 @@ class TestCreator(object): return not client_context.auth_enabled return True + @staticmethod + def serverless_ok(run_on_req): + serverless = run_on_req['serverless'] + if serverless == "require": + return client_context.serverless + elif serverless == "forbid": + return not client_context.serverless + else: # unset or "allow" + return True + def should_run_on(self, scenario_def): run_on = scenario_def.get('runOn', []) if not run_on: @@ -433,7 +456,8 @@ class TestCreator(object): if (self.valid_topology(req) and self.min_server_version(req) and self.max_server_version(req) and - self.valid_auth_enabled(req)): + self.valid_auth_enabled(req) and + self.serverless_ok(req)): return True return False