Skip to content

PYTHON-5055 - Convert test_client.py to unittest #2074

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2b058a2
PYTHON-5044 - Successive AsyncMongoClients on a single loop always ti…
NoahStapp Jan 17, 2025
45e74da
Only join executors on async
NoahStapp Jan 21, 2025
0296c20
Remove unneeded reset_async_client_context
NoahStapp Jan 21, 2025
266b0a3
Convert test_client to pytest
NoahStapp Jan 21, 2025
b6baf79
TestAsyncClientUnitTest done
NoahStapp Jan 21, 2025
cca705d
TestAsyncClientIntegrationTest converted
NoahStapp Jan 22, 2025
ae650e0
Asynchronous test_client.py done
NoahStapp Jan 22, 2025
8402799
Fix fixture scopes
NoahStapp Jan 22, 2025
048edf2
test_client.py conversion complete
NoahStapp Jan 23, 2025
84211e7
Lots of cleanup
NoahStapp Jan 23, 2025
d3c053c
Fix pytest invocations
NoahStapp Jan 23, 2025
d2620be
Workflow updates for asyncio tests
NoahStapp Jan 23, 2025
7ee03c9
Cleanup
NoahStapp Jan 23, 2025
b9d98c9
Typing fixes
NoahStapp Jan 23, 2025
41fe61e
Fix supports_secondary_read_pref
NoahStapp Jan 23, 2025
47b8c9d
Fix async pytest invocation for EG
NoahStapp Jan 23, 2025
9e115be
Remove executor closes
NoahStapp Jan 23, 2025
8602dde
Remove executor cancel from close
NoahStapp Jan 23, 2025
cc3f1ad
run-tests.sh fix for async
NoahStapp Jan 24, 2025
46e7436
Merge branch 'master' into PYTHON-5036
NoahStapp Jan 24, 2025
d906c3b
Fix uv.lock
NoahStapp Jan 24, 2025
8efa6ec
Remove mock.patch decorator
NoahStapp Jan 24, 2025
26b1178
test_iteration regex
NoahStapp Jan 24, 2025
9694239
run-tests.sh hacking for compatibility
NoahStapp Jan 24, 2025
9c3939b
run-tests.sh should use arrays for arguments
NoahStapp Jan 24, 2025
1a643c3
TEST_ARGS should also use array
NoahStapp Jan 24, 2025
90b4693
Undo unintended test/__init__.py changes
NoahStapp Jan 27, 2025
0791a8f
More run-tests.sh fixes
NoahStapp Jan 27, 2025
3f1a86c
Errexit hacking
NoahStapp Jan 27, 2025
077a1cb
Address review
NoahStapp Jan 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions .evergreen/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ set -o xtrace
AUTH=${AUTH:-noauth}
SSL=${SSL:-nossl}
TEST_SUITES=${TEST_SUITES:-}
TEST_ARGS="${*:1}"
TEST_ARGS=("${*:1}")

export PIP_QUIET=1 # Quiet by default
export PIP_PREFER_BINARY=1 # Prefer binary dists by default
Expand Down Expand Up @@ -206,6 +206,7 @@ if [ -n "$TEST_INDEX_MANAGEMENT" ]; then
TEST_SUITES="index_management"
fi

# shellcheck disable=SC2128
if [ -n "$TEST_DATA_LAKE" ] && [ -z "$TEST_ARGS" ]; then
TEST_SUITES="data_lake"
fi
Expand Down Expand Up @@ -235,7 +236,7 @@ if [ -n "$PERF_TEST" ]; then
TEST_SUITES="perf"
# PYTHON-4769 Run perf_test.py directly otherwise pytest's test collection negatively
# affects the benchmark results.
TEST_ARGS="test/performance/perf_test.py $TEST_ARGS"
TEST_ARGS+=("test/performance/perf_test.py")
fi

echo "Running $AUTH tests over $SSL with python $(uv python find)"
Expand All @@ -251,7 +252,7 @@ if [ -n "$COVERAGE" ] && [ "$PYTHON_IMPL" = "CPython" ]; then
# Keep in sync with combine-coverage.sh.
# coverage >=5 is needed for relative_files=true.
UV_ARGS+=("--group coverage")
TEST_ARGS="$TEST_ARGS --cov"
TEST_ARGS+=("--cov")
fi

if [ -n "$GREEN_FRAMEWORK" ]; then
Expand All @@ -265,15 +266,37 @@ PIP_QUIET=0 uv run ${UV_ARGS[*]} --with pip pip list
if [ -z "$GREEN_FRAMEWORK" ]; then
# Use --capture=tee-sys so pytest prints test output inline:
# https://docs.pytest.org/en/stable/how-to/capture-stdout-stderr.html
PYTEST_ARGS="-v --capture=tee-sys --durations=5 $TEST_ARGS"
PYTEST_ARGS=("-v" "--capture=tee-sys" "--durations=5" "${TEST_ARGS[@]}")
if [ -n "$TEST_SUITES" ]; then
PYTEST_ARGS="-m $TEST_SUITES $PYTEST_ARGS"
# Workaround until unittest -> pytest conversion is complete
if [[ "$TEST_SUITES" == *"default_async"* ]]; then
ASYNC_PYTEST_ARGS=("-m asyncio" "--junitxml=xunit-results/TEST-asyncresults.xml" "${PYTEST_ARGS[@]}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about coverage? Does that pick up both test runs automatically?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shows it's not working as expected. The second coverage run will overwrite the .coverage output file by default: https://coverage.readthedocs.io/en/7.6.10/cmd.html#data-file

Copy link
Contributor Author

@NoahStapp NoahStapp Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, does it make sense to use --append on the second run to keep the single file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative would be to use a separate file with COVERAGE_FILE and then coverage combine the two.

else
ASYNC_PYTEST_ARGS=("-m asyncio and $TEST_SUITES" "--junitxml=xunit-results/TEST-asyncresults.xml" "${PYTEST_ARGS[@]}")
fi
PYTEST_ARGS=("-m $TEST_SUITES and not asyncio" "${PYTEST_ARGS[@]}")
else
ASYNC_PYTEST_ARGS=("-m asyncio" "--junitxml=xunit-results/TEST-asyncresults.xml" "${PYTEST_ARGS[@]}")
fi
# Workaround until unittest -> pytest conversion is complete
set +o errexit
# shellcheck disable=SC2048
uv run ${UV_ARGS[*]} pytest "${PYTEST_ARGS[@]}"
exit_code=$?

# shellcheck disable=SC2048
uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS
uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}"
async_exit_code=$?
set -o errexit
if [ $async_exit_code -ne 5 ] && [ $async_exit_code -ne 0 ]; then
exit $async_exit_code
fi
if [ $exit_code -ne 0 ]; then
exit $exit_code
fi
else
# shellcheck disable=SC2048
uv run ${UV_ARGS[*]} green_framework_test.py $GREEN_FRAMEWORK -v $TEST_ARGS
uv run ${UV_ARGS[*]} green_framework_test.py $GREEN_FRAMEWORK -v "${TEST_ARGS[@]}"
fi

# Handle perf test post actions.
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/test-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ jobs:
run: |
if [[ "${{ matrix.python-version }}" == "3.13t" ]]; then
pytest -v --durations=5 --maxfail=10
pytest -v --durations=5 --maxfail=10 -m asyncio
else
just test
just test-async
fi

doctest:
Expand Down
4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ lint-manual:
test *args="-v --durations=5 --maxfail=10":
{{uv_run}} --extra test pytest {{args}}

[group('test')]
test-async *args="-v --durations=5 --maxfail=10 -m asyncio":
{{uv_run}} --extra test pytest {{args}}

[group('test')]
test-mockupdb *args:
{{uv_run}} -v --extra test --group mockupdb pytest -m mockupdb {{args}}
Expand Down
3 changes: 3 additions & 0 deletions pymongo/asynchronous/mongo_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,9 @@ def __next__(self) -> NoReturn:
raise TypeError("'AsyncMongoClient' object is not iterable")

next = __next__
if not _IS_SYNC:
anext = next
__anext__ = next

async def _server_property(self, attr_name: str) -> Any:
"""An attribute of the current server's description.
Expand Down
3 changes: 3 additions & 0 deletions pymongo/synchronous/mongo_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1414,6 +1414,9 @@ def __next__(self) -> NoReturn:
raise TypeError("'MongoClient' object is not iterable")

next = __next__
if not _IS_SYNC:
next = next
__next__ = next

def _server_property(self, attr_name: str) -> Any:
"""An attribute of the current server's description.
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ zstd = ["requirements/zstd.txt"]

[tool.pytest.ini_options]
minversion = "7"
addopts = ["-ra", "--strict-config", "--strict-markers", "--junitxml=xunit-results/TEST-results.xml", "-m default or default_async"]
addopts = ["-ra", "--strict-config", "--strict-markers", "--junitxml=xunit-results/TEST-results.xml", "-m default or default_async and not asyncio"]
testpaths = ["test"]
log_cli_level = "INFO"
faulthandler_timeout = 1500
Expand Down Expand Up @@ -135,6 +135,8 @@ markers = [
"mockupdb: tests that rely on mockupdb",
"default: default test suite",
"default_async: default async test suite",
"unit: tests that don't require a connection to MongoDB",
"integration: tests that require a connection to MongoDB",
]

[tool.mypy]
Expand Down
27 changes: 23 additions & 4 deletions test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
"""Synchronous test suite for pymongo, bson, and gridfs."""
from __future__ import annotations

import asyncio
import gc
import logging
import multiprocessing
import os
import signal
Expand All @@ -26,7 +24,6 @@
import sys
import threading
import time
import traceback
import unittest
import warnings
from asyncio import iscoroutinefunction
Expand Down Expand Up @@ -518,6 +515,12 @@ def require_data_lake(self, func):
func=func,
)

@property
def is_not_mmap(self):
if self.is_mongos:
return True
return self.storage_engine != "mmapv1"

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
Expand Down Expand Up @@ -571,6 +574,10 @@ 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)

@property
def secondaries_count(self):
return 0 if not self.client else len(self.client.secondaries)

def require_secondaries_count(self, count):
"""Run a test only if the client is connected to a replica set that has
`count` secondaries.
Expand All @@ -589,7 +596,7 @@ def supports_secondary_read_pref(self):
if self.has_secondaries:
return True
if self.is_mongos:
shard = self.client.config.shards.find_one()["host"] # type:ignore[index]
shard = (self.client.config.shards.find_one())["host"] # type:ignore[index]
num_members = shard.count(",") + 1
return num_members > 1
return False
Expand Down Expand Up @@ -874,6 +881,18 @@ def reset_client_context():
client_context._init_client()


class PyMongoTestCasePyTest:
@contextmanager
def fail_point(self, client, command_args):
cmd_on = SON([("configureFailPoint", "failCommand")])
cmd_on.update(command_args)
client.admin.command(cmd_on)
try:
yield
finally:
client.admin.command("configureFailPoint", cmd_on["configureFailPoint"], mode="off")


class PyMongoTestCase(unittest.TestCase):
def assertEqualCommand(self, expected, actual, msg=None):
self.assertEqual(sanitize_cmd(expected), sanitize_cmd(actual), msg)
Expand Down
31 changes: 26 additions & 5 deletions test/asynchronous/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
"""Asynchronous test suite for pymongo, bson, and gridfs."""
from __future__ import annotations

import asyncio
import gc
import logging
import multiprocessing
import os
import signal
Expand All @@ -26,7 +24,6 @@
import sys
import threading
import time
import traceback
import unittest
import warnings
from asyncio import iscoroutinefunction
Expand Down Expand Up @@ -520,6 +517,12 @@ def require_data_lake(self, func):
func=func,
)

@property
def is_not_mmap(self):
if self.is_mongos:
return True
return self.storage_engine != "mmapv1"

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
Expand Down Expand Up @@ -573,6 +576,10 @@ 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)

@property
async def secondaries_count(self):
return 0 if not self.client else len(await self.client.secondaries)

def require_secondaries_count(self, count):
"""Run a test only if the client is connected to a replica set that has
`count` secondaries.
Expand All @@ -588,10 +595,10 @@ async def check():

@property
async def supports_secondary_read_pref(self):
if self.has_secondaries:
if await self.has_secondaries:
return True
if self.is_mongos:
shard = await self.client.config.shards.find_one()["host"] # type:ignore[index]
shard = (await self.client.config.shards.find_one())["host"] # type:ignore[index]
num_members = shard.count(",") + 1
return num_members > 1
return False
Expand Down Expand Up @@ -876,6 +883,20 @@ async def reset_client_context():
await async_client_context._init_client()


class AsyncPyMongoTestCasePyTest:
@asynccontextmanager
async def fail_point(self, client, command_args):
cmd_on = SON([("configureFailPoint", "failCommand")])
cmd_on.update(command_args)
await client.admin.command(cmd_on)
try:
yield
finally:
await client.admin.command(
"configureFailPoint", cmd_on["configureFailPoint"], mode="off"
)


class AsyncPyMongoTestCase(unittest.IsolatedAsyncioTestCase):
def assertEqualCommand(self, expected, actual, msg=None):
self.assertEqual(sanitize_cmd(expected), sanitize_cmd(actual), msg)
Expand Down
Loading
Loading