393 lines
15 KiB
Python
393 lines
15 KiB
Python
# Copyright 2012-2014 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 Motor by testing that Synchro, a fake PyMongo implementation built on
|
|
top of Motor, passes the same unittests as PyMongo.
|
|
|
|
This program monkey-patches sys.modules, so run it alone, rather than as part
|
|
of a larger test suite.
|
|
"""
|
|
|
|
import fnmatch
|
|
import importlib
|
|
import importlib.abc
|
|
import importlib.machinery
|
|
import os
|
|
import re
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import synchro
|
|
|
|
excluded_modules = [
|
|
# Exclude some PyMongo tests that can't be applied to Synchro.
|
|
"test.test_examples",
|
|
"test.test_threads",
|
|
"test.test_pooling",
|
|
"test.test_saslprep",
|
|
# Complex PyMongo-specific mocking.
|
|
"test.test_replica_set_reconfig",
|
|
# Accesses PyMongo internals.
|
|
"test.test_retryable_writes",
|
|
# Accesses PyMongo internals. Tested directly in Motor.
|
|
"test.test_session",
|
|
# Deprecated in PyMongo, removed in Motor 2.0.
|
|
"test.test_gridfs",
|
|
# Skip mypy/typing tests.
|
|
"test.test_typing",
|
|
# Relies of specifics of __all__
|
|
"test.test_default_exports",
|
|
# Motor does not support CSOT.
|
|
"test.test_csot",
|
|
]
|
|
|
|
|
|
# Patterns consist of the TestClass.test_method
|
|
# You can use * for any portion of the class or method pattern
|
|
excluded_tests = [
|
|
# Motor's reprs aren't the same as PyMongo's.
|
|
"*.test_repr",
|
|
"TestClient.test_unix_socket",
|
|
# Motor extends the handshake metadata.
|
|
"ClientUnitTest.test_metadata",
|
|
# Lazy-connection tests require multithreading; we test concurrent
|
|
# lazy connection directly.
|
|
"TestClientLazyConnect.*",
|
|
# Motor doesn't support forking or threading.
|
|
"*.test_interrupt_signal",
|
|
"TestSCRAM.test_scram_threaded",
|
|
"TestGSSAPI.test_gssapi_threaded",
|
|
"*.test_concurrent_close",
|
|
"TestUnifiedInterruptInUsePoolClear.*",
|
|
# These are in test_gridfs_bucket.
|
|
"TestGridfs.test_threaded_reads",
|
|
"TestGridfs.test_threaded_writes",
|
|
# Can't do MotorCollection(name, create=True), Motor constructors do no I/O.
|
|
"TestCollection.test_create",
|
|
# Requires indexing / slicing cursors, which Motor doesn't do, see MOTOR-84.
|
|
"TestCollection.test_min_query",
|
|
"TestCursor.test_clone",
|
|
"TestCursor.test_clone_empty",
|
|
"TestCursor.test_getitem_numeric_index",
|
|
"TestCursor.test_getitem_slice_index",
|
|
"TestCursor.test_tailable",
|
|
"TestRawBatchCursor.test_get_item",
|
|
"TestRawBatchCommandCursor.test_get_item",
|
|
# No context-manager protocol for MotorCursor.
|
|
"TestCursor.test_with_statement",
|
|
# Motor's cursors initialize lazily.
|
|
"TestRawBatchCommandCursor.test_monitoring",
|
|
# Can't iterate a GridOut in Motor.
|
|
"TestGridFile.test_iterator",
|
|
"TestGridfs.test_missing_length_iter",
|
|
# No context-manager protocol for MotorGridIn, and can't set attrs.
|
|
"TestGridFile.test_context_manager",
|
|
"TestGridFile.test_grid_in_default_opts",
|
|
"TestGridFile.test_set_after_close",
|
|
# GridOut always connects lazily in Motor.
|
|
"TestGridFile.test_grid_out_lazy_connect",
|
|
"TestGridfs.test_gridfs_lazy_connect", # In test_gridfs_bucket.
|
|
# Complex PyMongo-specific mocking.
|
|
"*.test_wire_version",
|
|
"TestClient.test_heartbeat_frequency_ms",
|
|
"TestExhaustCursor.*",
|
|
"TestHeartbeatMonitoring.*",
|
|
"TestMongoClientFailover.*",
|
|
"TestMongosLoadBalancing.*",
|
|
"TestSSL.test_system_certs_config_error",
|
|
"TestCMAP.test_cmap_wait_queue_timeout_must_aggressively_timeout_threads_enqueued_longer_than_waitQueueTimeoutMS",
|
|
# Accesses PyMongo internals.
|
|
"TestClient.test_close_kills_cursors",
|
|
"TestClient.test_stale_getmore",
|
|
"TestClient.test_direct_connection",
|
|
"TestCollection.test_aggregation_cursor",
|
|
"TestCommandAndReadPreference.*",
|
|
"TestCommandMonitoring.test_get_more_failure",
|
|
"TestCommandMonitoring.test_sensitive_commands",
|
|
"TestCursor.test_allow_disk_use",
|
|
"TestCursor.test_close_kills_cursor_synchronously",
|
|
"TestCursor.test_delete_not_initialized",
|
|
"TestGridFile.test_grid_out_cursor_options",
|
|
"TestGridFile.test_survive_cursor_not_found",
|
|
"TestMaxStaleness.test_last_write_date",
|
|
"TestSelections.test_bool",
|
|
"TestCollation.*",
|
|
"TestCollection.test_find_one_and_write_concern",
|
|
"TestCollection.test_write_error_text_handling",
|
|
"TestBinary.test_uuid_queries",
|
|
"TestCursor.test_comment",
|
|
"TestCursor.test_where",
|
|
"TestGridfs.test_gridfs_find",
|
|
# Uses patching on grid_file._UPLOAD_BUFFER_SIZE which fails in synchro.
|
|
"TestGridfs.test_abort",
|
|
"TestGridfs.test_upload_batching",
|
|
"TestGridfs.test_upload_bulk_write_error",
|
|
"TestKmsTLSOptions.test_05_tlsDisableOCSPEndpointCheck_is_permitted",
|
|
# Tests that use "authenticate" or "logoout", removed in Motor 2.0.
|
|
"TestSASLPlain.test_sasl_plain_bad_credentials",
|
|
"TestSCRAM.test_scram",
|
|
"TestSCRAMSHA1.test_scram_sha1",
|
|
# Uses "collection_names", deprecated in PyMongo, removed in Motor 2.0.
|
|
"TestSingleSecondaryOk.test_reads_from_secondary",
|
|
# Slow.
|
|
"TestDatabase.test_list_collection_names",
|
|
# MOTOR-425 these tests fail with duplicate key errors.
|
|
"TestClusterChangeStreamsWCustomTypes.*",
|
|
"TestCollectionChangeStreamsWCustomTypes.*",
|
|
"TestDatabaseChangeStreamsWCustomTypes.*",
|
|
# Tests that use warnings.catch_warnings which don't show up in Motor.
|
|
"TestCursor.test_min_max_without_hint",
|
|
# TODO: MOTOR-606
|
|
"TestTransactionsConvenientAPI.*",
|
|
"TestTransactions.test_create_collection",
|
|
# Motor's change streams need Python 3.5 to support async iteration but
|
|
# these change streams tests spawn threads which don't work without an
|
|
# IO loop.
|
|
"*.test_next_blocks",
|
|
"*.test_aggregate_cursor_blocks",
|
|
# Can't run these tests because they use threads.
|
|
"*.test_ignore_stale_connection_errors",
|
|
"*.test_pool_paused_error_is_retryable",
|
|
# Needs synchro.GridFS class, see MOTOR-609.
|
|
"TestTransactions.test_gridfs_does_not_support_transactions",
|
|
# PYTHON-3228 _tmp_session should validate session input
|
|
"*.test_helpers_with_let",
|
|
# Relies on comment being in the method signatures, which would force use
|
|
# to rewrite much of AgnosticCollection.
|
|
"*.test_collection_helpers",
|
|
"*.test_database_helpers",
|
|
"*.test_client_helpers",
|
|
# This test is too slow given all of the wrapping logic.
|
|
"*.test_transaction_starts_with_batched_write",
|
|
# This test is too flaky given all the wrapping logic.
|
|
"TestProse.test_load_balancing",
|
|
# This feature is going away in PyMongo 5
|
|
"*.test_iteration",
|
|
# MD5 is deprecated
|
|
"*.test_md5",
|
|
# Causes a deadlock.
|
|
"TestFork.*",
|
|
# Also causes a deadlock.
|
|
"TestClientSimple.test_fork",
|
|
# This method requires credentials.
|
|
"TestOnDemandAWSCredentials.test_02_success",
|
|
# These methods are picked up by nose despite not being a unittest.
|
|
"TestRewrapWithSeparateClientEncryption.run_test",
|
|
"TestCustomEndpoint.run_test_expected_success",
|
|
"TestDataKeyDoubleEncryption.run_test",
|
|
# These tests are failing right now.
|
|
"TestUnifiedFindShutdownError.test_Concurrent_shutdown_error_on_find",
|
|
"TestUnifiedInsertShutdownError.test_Concurrent_shutdown_error_on_insert",
|
|
"TestUnifiedPoolClearedError.test_PoolClearedError_does_not_mark_server_unknown",
|
|
# These tests have hard-coded values that differ from motor.
|
|
"TestClient.test_handshake*",
|
|
# This test is not a valid unittest target.
|
|
"TestRangeQueryProse.run_test_cases",
|
|
# The test logic interferes with the SynchroLoader.
|
|
"ClientUnitTest.test_detected_environment_logging",
|
|
"ClientUnitTest.test_detected_environment_warning",
|
|
# The batch_size portion of this test does not work with synchro.
|
|
"TestCursor.test_to_list_length",
|
|
# These tests hang due to internal incompatibilities in to_list.
|
|
"TestCursor.test_command_cursor_to_list*",
|
|
# Motor does not allow calling to_list on a tailable cursor.
|
|
"TestCursor.test_max_await_time_ms",
|
|
"TestCursor.test_to_list_tailable",
|
|
# This test relies on a private api"
|
|
"TestTransactions.test_transaction_pool_cleared_error_labelled_transient",
|
|
# Newer tests not implemented in Motor
|
|
"TestClient.test_repr_srv_host",
|
|
"TestGridfs.test_delete_by_name",
|
|
"TestGridfs.test_rename_by_name",
|
|
"TestGridfsDeleteByName.*",
|
|
"TestGridfsRenameByName.*",
|
|
"TestMongosAndReadPreference.test_read_preference_hedge_deprecated",
|
|
"TestUnifiedTestFormatValidPassKmsProvidersExplicitKmsCredentials.*",
|
|
"TestUnifiedTestFormatValidPassKmsProvidersMixedKmsCredentialFields.*",
|
|
"TestUnifiedTestFormatValidPassKmsProvidersPlaceholderKmsCredentials.*",
|
|
]
|
|
|
|
|
|
excluded_modules_matched = set()
|
|
excluded_tests_matched = set()
|
|
|
|
# Validate the exclude lists.
|
|
for item in excluded_modules:
|
|
if not re.match(r"^test\.[a-zA-Z_]+$", item):
|
|
raise ValueError(f"Improper excluded module {item}")
|
|
for item in excluded_tests:
|
|
cls_pattern, _, mod_pattern = item.partition(".")
|
|
if cls_pattern != "*" and not re.match(r"^[a-zA-Z\d]+$", cls_pattern):
|
|
raise ValueError(f"Ill-formatted excluded test: {item}")
|
|
if mod_pattern != "*" and not re.match(r"^[a-zA-Z\d_\*]+$", mod_pattern):
|
|
raise ValueError(f"Ill-formatted excluded test: {item}")
|
|
|
|
|
|
class SynchroModuleFinder(importlib.abc.MetaPathFinder):
|
|
def __init__(self):
|
|
self._loader = SynchroModuleLoader()
|
|
|
|
def find_spec(self, fullname, path, target=None):
|
|
if self._loader.patch_spec(fullname):
|
|
return importlib.machinery.ModuleSpec(fullname, self._loader)
|
|
|
|
# Let regular module search continue.
|
|
return None
|
|
|
|
|
|
class SynchroModuleLoader(importlib.abc.Loader):
|
|
def patch_spec(self, fullname):
|
|
parts = fullname.split(".")
|
|
if ("pymongo" in parts or "gridfs" in parts) and "asynchronous" not in parts:
|
|
return True
|
|
|
|
return False
|
|
|
|
def exec_module(self, module):
|
|
pass
|
|
|
|
def create_module(self, spec):
|
|
if self.patch_spec(spec.name):
|
|
return synchro
|
|
|
|
# Let regular module search continue.
|
|
return None
|
|
|
|
|
|
class SynchroPytestPlugin:
|
|
def pytest_collection_modifyitems(self, session, config, items):
|
|
for item in items[:]:
|
|
if not want_module(item.module):
|
|
item.addSkip("", reason="Synchro excluded module")
|
|
continue
|
|
fn = item.function
|
|
if item.parent == item.module:
|
|
if not want_function(fn):
|
|
item.addSkip("", reason="Synchro excluded function")
|
|
elif not want_method(fn, item.parent.name):
|
|
item.addSkip("", reason="Synchro excluded method")
|
|
|
|
|
|
def want_module(module):
|
|
# Depending on PYTHONPATH, Motor's direct tests may be imported - don't
|
|
# run them now.
|
|
if module.__name__.startswith("test.test_motor_"):
|
|
return False
|
|
|
|
for module_name in excluded_modules:
|
|
if module_name.endswith("*"):
|
|
if module.__name__.startswith(module_name.rstrip("*")):
|
|
# E.g., test_motor_cursor matches "test_motor_*".
|
|
excluded_modules_matched.add(module_name)
|
|
return False
|
|
|
|
elif module.__name__ == module_name:
|
|
excluded_modules_matched.add(module_name)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def want_function(fn):
|
|
# PyMongo's test generators run at import time; tell pytest not to run
|
|
# them as unittests.
|
|
if fn.__name__ in ("test_cases",):
|
|
return False
|
|
return True
|
|
|
|
|
|
def want_method(method, classname):
|
|
for excluded_name in excluded_tests:
|
|
# Should we exclude this method's whole TestCase?
|
|
cls_pattern, _, method_pattern = excluded_name.partition(".")
|
|
suite_matches = fnmatch.fnmatch(classname, cls_pattern)
|
|
|
|
# Should we exclude this particular method?
|
|
method_matches = method.__name__ == method_pattern or fnmatch.fnmatch(
|
|
method.__name__, method_pattern
|
|
)
|
|
if suite_matches and method_matches:
|
|
excluded_tests_matched.add(excluded_name)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Monkey-patch all pymongo's unittests so they think Synchro is the
|
|
# real PyMongo.
|
|
sys.meta_path[0:0] = [SynchroModuleFinder()]
|
|
# Delete the cached pymongo/gridfs modules so that SynchroModuleFinder will
|
|
# be invoked in Python 3, see
|
|
# https://docs.python.org/3/reference/import.html#import-hooks
|
|
for n in [
|
|
"pymongo",
|
|
"pymongo.collection",
|
|
"pymongo.client_session",
|
|
"pymongo.command_cursor",
|
|
"pymongo.change_stream",
|
|
"pymongo.cursor",
|
|
"pymongo.encryption",
|
|
"pymongo.encryption_options",
|
|
"pymongo.mongo_client",
|
|
"pymongo.database",
|
|
"pymongo.synchronous.srv_resolver",
|
|
"pymongo.synchronous.collection",
|
|
"pymongo.synchronous.client_session",
|
|
"pymongo.synchronous.command_cursor",
|
|
"pymongo.synchronous.change_stream",
|
|
"pymongo.synchronous.cursor",
|
|
"pymongo.synchronous.encryption",
|
|
"pymongo.synchronous.mongo_client",
|
|
"pymongo.synchronous.database",
|
|
"gridfs",
|
|
"gridfs.grid_file",
|
|
"gridfs.synchronous.grid_file",
|
|
]:
|
|
sys.modules.pop(n)
|
|
|
|
# Prep the xUnit report dir.
|
|
root = Path(__file__).absolute().parent.parent
|
|
xunit_dir = root / "xunit-results"
|
|
if xunit_dir.exists():
|
|
shutil.rmtree(xunit_dir)
|
|
|
|
# Run the tests from the pymongo target dir with our custom plugin.
|
|
os.chdir(sys.argv[1])
|
|
if "-m" in sys.argv[2:]:
|
|
markers = []
|
|
else:
|
|
markers = ["-m", "default"]
|
|
code = pytest.main(
|
|
sys.argv[2:] + markers + ["-p", "no:warnings"], plugins=[SynchroPytestPlugin()]
|
|
)
|
|
|
|
if code != 0:
|
|
sys.exit(code)
|
|
|
|
# Copy over the xUnit report.
|
|
xunit_dir.mkdir()
|
|
target = Path(sys.argv[1]) / "xunit-results"
|
|
shutil.copy(target / "TEST-results.xml", xunit_dir / "TEST-results.xml")
|
|
|
|
if os.environ.get("CHECK_EXCLUDE_PATTERNS"):
|
|
unused_module_pats = set(excluded_modules) - excluded_modules_matched
|
|
assert not unused_module_pats, "Unused module patterns: {unused_module_pats}"
|
|
|
|
unused_test_pats = set(excluded_tests) - excluded_tests_matched
|
|
assert not unused_test_pats, f"Unused test patterns: {unused_test_pats}"
|