Skip to content

PYTHON-5784 Coverage increase for periodic_executor.py#2771

Draft
aclark4life wants to merge 9 commits intomongodb:masterfrom
aclark4life:PYTHON-5784
Draft

PYTHON-5784 Coverage increase for periodic_executor.py#2771
aclark4life wants to merge 9 commits intomongodb:masterfrom
aclark4life:PYTHON-5784

Conversation

@aclark4life
Copy link
Copy Markdown
Contributor

@aclark4life aclark4life commented Apr 23, 2026

PYTHON-5784

Changes in this PR

Adds test/asynchronous/test_periodic_executor.py (synchro'd to test/test_periodic_executor.py) with unit tests for pymongo/periodic_executor.py, covering both PeriodicExecutor and AsyncPeriodicExecutor:

  • join() before open() is a safe no-op.
  • Target returning False stops the executor.
  • Target raising an exception stops the executor (verified by asserting target is called exactly once).
  • skip_sleep() causes the next iteration to run without waiting the full interval.
  • wake() interrupts the current sleep and triggers an early run.
  • open() after the target returned False restarts the executor.

Test Plan

Added unit tests using asyncio.Event / threading.Event for deterministic synchronization. The exception test swaps threading.excepthook to a no-op so the intentional RuntimeError in the worker thread is not surfaced by pytest's threadexception plugin (no-op for the async executor since no thread is involved).

uv run --extra test python -m pytest test/asynchronous/test_periodic_executor.py test/test_periodic_executor.py -v

Checklist

Checklist for Author

  • Did you update the changelog (if necessary)?
  • Is there test coverage?
  • Is any followup work tracked in a JIRA ticket? If so, add link(s).

Checklist for Reviewer

  • Does the title of the PR reference a JIRA Ticket?
  • Do you fully understand the implementation? (Would you be comfortable explaining how this code works to someone else?)
  • Is all relevant documentation (README or docstring) updated?

Copilot AI review requested due to automatic review settings April 23, 2026 21:34
@aclark4life aclark4life requested a review from a team as a code owner April 23, 2026 21:34
@aclark4life aclark4life requested a review from NoahStapp April 23, 2026 21:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a dedicated unit test module for pymongo.periodic_executor, targeting both the threading-based PeriodicExecutor and asyncio-based AsyncPeriodicExecutor, plus module-level executor registration/shutdown helpers, to improve overall coverage.

Changes:

  • Adds new unit tests for PeriodicExecutor lifecycle and control methods (open/close/join/wake/update_interval/skip_sleep, stop conditions).
  • Adds new unit tests for AsyncPeriodicExecutor lifecycle and stop conditions.
  • Adds unit tests for module-level _register_executor() and _shutdown_executors() behavior.

Comment thread test/test_periodic_executor.py Outdated
Comment thread test/test_periodic_executor.py Outdated
Comment thread test/test_periodic_executor.py Outdated
Comment thread test/test_periodic_executor.py Outdated
Comment thread test/test_periodic_executor.py Outdated
Comment thread test/test_periodic_executor.py Outdated
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 33.17%. Comparing base (f4219bd) to head (71a240c).

❗ There is a different number of reports uploaded between BASE (f4219bd) and HEAD (71a240c). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (f4219bd) HEAD (71a240c)
1 0
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #2771       +/-   ##
===========================================
- Coverage   82.68%   33.17%   -49.51%     
===========================================
  Files         141      136        -5     
  Lines       24406    24383       -23     
  Branches     4176     4176               
===========================================
- Hits        20179     8088    -12091     
- Misses       3328    15815    +12487     
+ Partials      899      480      -419     
Flag Coverage Δ
auth-aws-rhel8-test-auth-aws-rapid-web-identity-python3.14-cov 26.78% <ø> (-8.24%) ⬇️
auth-aws-win64-test-auth-aws-rapid-web-identity-python3.14-cov 26.80% <ø> (-8.24%) ⬇️
auth-enterprise-macos-test-standard-auth-latest-python3.11-auth-ssl-sharded-cluster-cov 26.77% <ø> (-16.98%) ⬇️
auth-enterprise-rhel8-test-standard-auth-latest-python3.11-auth-ssl-sharded-cluster-cov 26.78% <ø> (-16.98%) ⬇️
auth-enterprise-win64-test-standard-auth-latest-python3.11-auth-ssl-sharded-cluster-cov 26.81% <ø> (-16.98%) ⬇️
auth-oidc-local-ubuntu-22-test-auth-oidc-default 26.78% <ø> (?)
compression-snappy-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.57%) ⬇️
compression-snappy-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.78% <ø> (-34.70%) ⬇️
compression-snappy-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 26.78% <ø> (-34.32%) ⬇️
compression-snappy-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.23%) ⬇️
compression-zlib-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.57%) ⬇️
compression-zlib-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.78% <ø> (-34.73%) ⬇️
compression-zlib-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 26.78% <ø> (-34.32%) ⬇️
compression-zlib-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.24%) ⬇️
compression-zstd-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.58%) ⬇️
compression-zstd-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.78% <ø> (-34.70%) ⬇️
compression-zstd-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 26.78% <ø> (-34.32%) ⬇️
compression-zstd-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.24%) ⬇️
compression-zstd-ubuntu-22-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.21%) ⬇️
coverage-report-coverage-report 32.85% <ø> (-49.57%) ⬇️
disable-test-commands-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.58%) ⬇️
disable-test-commands-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.78% <ø> (-34.74%) ⬇️
disable-test-commands-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 26.78% <ø> (-34.32%) ⬇️
disable-test-commands-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.23%) ⬇️
encryption-crypt_shared-macos-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 27.07% <ø> (?)
encryption-crypt_shared-macos-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 27.07% <ø> (?)
encryption-crypt_shared-macos-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 27.07% <ø> (?)
encryption-crypt_shared-rhel8-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 27.07% <ø> (?)
encryption-crypt_shared-rhel8-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 27.07% <ø> (?)
encryption-crypt_shared-rhel8-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 27.07% <ø> (?)
encryption-crypt_shared-win64-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 27.09% <ø> (?)
encryption-crypt_shared-win64-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 27.09% <ø> (?)
encryption-crypt_shared-win64-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 27.09% <ø> (?)
encryption-macos-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 27.07% <ø> (?)
encryption-macos-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 27.07% <ø> (?)
encryption-macos-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 27.07% <ø> (?)
encryption-pyopenssl-rhel8-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 27.26% <ø> (?)
encryption-pyopenssl-rhel8-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 27.26% <ø> (?)
encryption-pyopenssl-rhel8-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 27.26% <ø> (?)
encryption-rhel8-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 27.07% <ø> (?)
encryption-rhel8-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 27.07% <ø> (?)
encryption-rhel8-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 27.07% <ø> (?)
encryption-win64-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 27.09% <ø> (?)
encryption-win64-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 27.09% <ø> (?)
encryption-win64-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 27.09% <ø> (?)
load-balancer-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 27.24% <ø> (?)
mongodb-latest-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-34.69%) ⬇️
mongodb-latest-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 26.78% <ø> (-32.58%) ⬇️
mongodb-latest-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-32.73%) ⬇️
mongodb-latest-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 26.78% <ø> (-32.82%) ⬇️
mongodb-latest-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 26.78% <ø> (-34.64%) ⬇️
mongodb-rapid-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-34.70%) ⬇️
mongodb-rapid-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 26.78% <ø> (-32.58%) ⬇️
mongodb-rapid-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-32.73%) ⬇️
mongodb-rapid-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 26.78% <ø> (-32.82%) ⬇️
mongodb-rapid-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 26.78% <ø> (-34.65%) ⬇️
mongodb-v4.2-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-30.40%) ⬇️
mongodb-v4.2-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 26.78% <ø> (-28.83%) ⬇️
mongodb-v4.2-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-30.29%) ⬇️
mongodb-v4.2-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 26.78% <ø> (-30.39%) ⬇️
mongodb-v4.2-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 26.78% <ø> (-30.58%) ⬇️
mongodb-v4.4-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-32.79%) ⬇️
mongodb-v4.4-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 26.78% <ø> (-30.81%) ⬇️
mongodb-v4.4-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-30.80%) ⬇️
mongodb-v4.4-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 26.78% <ø> (-30.88%) ⬇️
mongodb-v4.4-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 26.78% <ø> (-32.57%) ⬇️
mongodb-v5.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-32.99%) ⬇️
mongodb-v5.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 26.78% <ø> (-30.97%) ⬇️
mongodb-v5.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-31.00%) ⬇️
mongodb-v5.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 26.78% <ø> (-31.11%) ⬇️
mongodb-v5.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 26.78% <ø> (-32.81%) ⬇️
mongodb-v6.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-33.00%) ⬇️
mongodb-v6.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 26.78% <ø> (-30.97%) ⬇️
mongodb-v6.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-31.02%) ⬇️
mongodb-v6.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 26.78% <ø> (-31.16%) ⬇️
mongodb-v6.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 26.78% <ø> (-32.97%) ⬇️
mongodb-v7.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-33.03%) ⬇️
mongodb-v7.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 26.78% <ø> (-30.96%) ⬇️
mongodb-v7.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-31.07%) ⬇️
mongodb-v7.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 26.78% <ø> (-31.15%) ⬇️
mongodb-v7.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 26.78% <ø> (-32.97%) ⬇️
mongodb-v8.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-34.70%) ⬇️
mongodb-v8.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 26.78% <ø> (-32.55%) ⬇️
mongodb-v8.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 26.78% <ø> (-32.73%) ⬇️
mongodb-v8.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 26.78% <ø> (-32.82%) ⬇️
mongodb-v8.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 26.78% <ø> (-34.63%) ⬇️
no-c-ext-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.81% <ø> (-33.74%) ⬇️
no-c-ext-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.81% <ø> (-35.87%) ⬇️
no-c-ext-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 26.81% <ø> (-35.49%) ⬇️
no-c-ext-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.81% <ø> (-33.41%) ⬇️
ocsp-rhel8-test-ocsp-ecdsa-valid-cert-server-staples-latest-python3.14-cov 26.97% <ø> (-7.24%) ⬇️
ocsp-rhel8-test-ocsp-rsa-valid-cert-server-staples-latest-python3.14-cov 26.97% <ø> (-7.24%) ⬇️
pyopenssl-macos-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.97% <ø> (?)
pyopenssl-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.97% <ø> (?)
pyopenssl-win64-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.99% <ø> (?)
stable-api-accept-v2-rhel8-auth-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.57%) ⬇️
stable-api-accept-v2-rhel8-auth-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.24%) ⬇️
stable-api-require-v1-rhel8-auth-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.79% <ø> (-32.53%) ⬇️
stable-api-require-v1-rhel8-auth-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 26.79% <ø> (-34.16%) ⬇️
stable-api-require-v1-rhel8-auth-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.79% <ø> (-32.18%) ⬇️
storage-inmemory-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.57%) ⬇️
storage-inmemory-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.78% <ø> (-32.24%) ⬇️
test-macos-arm64-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.77% <ø> (-32.56%) ⬇️
test-macos-arm64-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.77% <ø> (-34.75%) ⬇️
test-macos-arm64-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 26.77% <ø> (-34.32%) ⬇️
test-macos-arm64-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.77% <ø> (-32.20%) ⬇️
test-macos-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.77% <ø> (-32.54%) ⬇️
test-macos-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.77% <ø> (-34.72%) ⬇️
test-macos-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 26.77% <ø> (-34.31%) ⬇️
test-macos-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.77% <ø> (-32.21%) ⬇️
test-numpy-macos-arm64-test-numpy-python3.14-python3.14-cov 32.61% <ø> (-0.02%) ⬇️
test-numpy-macos-test-numpy-python3.14-python3.14-cov 32.61% <ø> (ø)
test-numpy-rhel8-test-numpy-python3.14-python3.14-cov 32.61% <ø> (ø)
test-numpy-win32-test-numpy-python3.14-python3.14-cov 32.59% <ø> (ø)
test-numpy-win64-test-numpy-python3.14-python3.14-cov 32.59% <ø> (ø)
test-win32-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.80% <ø> (-32.41%) ⬇️
test-win32-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.80% <ø> (-34.65%) ⬇️
test-win32-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 26.80% <ø> (-34.28%) ⬇️
test-win32-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.80% <ø> (-32.07%) ⬇️
test-win64-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 26.80% <ø> (-32.40%) ⬇️
test-win64-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 26.80% <ø> (-34.65%) ⬇️
test-win64-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 26.80% <ø> (-34.28%) ⬇️
test-win64-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 26.80% <ø> (-32.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Comment thread test/test_periodic_executor.py Outdated
Comment on lines +52 to +56
async def _default_target():
return True

if target is None:
target = _default_target
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does this need to differ from the way _make_sync above does it?

Comment thread test/test_periodic_executor.py Outdated
Comment on lines +62 to +63
def _run(coroutine):
return asyncio.run(coroutine)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The individual tests that use asynchronous methods should use AsyncUnitTest or another appropriate test class from test/asynchronous/__init__.py instead of this. Each asyncio.run() call adds a significant amount of runtime overhead to setup and teardown the event loop.

Comment thread test/test_periodic_executor.py Outdated

class TestPeriodicExecutorRepr(unittest.TestCase):
def test_repr_contains_class_and_name(self):
ex = _make_sync(name="myexec")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can move all the _make_sync() calls into an asyncSetUp/setUp method on a base class shared by all of the test classes here.

finally:
ex.close()
ex.join(timeout=2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similarly, safe closing of each executor can be done in an asyncTearDown/tearDown method on a base class.

Comment thread test/test_periodic_executor.py Outdated
# ---------------------------------------------------------------------------


class TestAsyncPeriodicExecutorRepr(unittest.TestCase):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we move the AsyncPeriodicExecutor tests into test/asynchronous/ and generate the synchronous ones using synchro? That would half the amount of code we need to maintain.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should #2774 get the same treatment?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes. The only tests that should not go through synchro are tests that don't need modification between async and sync, as well as tests against common modules shared between them.

@aclark4life aclark4life requested a review from NoahStapp April 29, 2026 18:32
await self.executor.join(timeout=2)


class TestAsyncPeriodicExecutorRepr(AsyncUnitTest):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This class should also inherit from AsyncPeriodicExecutorTestBase and use its setup/teardown methods.

self.assertIn("exec", executor_repr)


class TestAsyncPeriodicExecutorBasic(AsyncPeriodicExecutorTestBase):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's the intended purpose of having so many separate classes, each with a couple of tests? We generally only separate tests inside files into separate classes if the groupings need different infrastructure (unit vs integration) or specific setup/configuration common to them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This question still stands @aclark4life.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This question still stands @aclark4life.

No good reason, especially in the context of removing non-behavior-checking tests. I'll push a single class (plus base class) unless we come across a reason to add more.



class TestAsyncPeriodicExecutorBasic(AsyncPeriodicExecutorTestBase):
async def test_update_interval(self):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test and the following don't test behavior but implementation.



class TestAsyncPeriodicExecutorLifecycle(AsyncPeriodicExecutorTestBase):
async def test_open_starts_worker(self):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here, this is testing implementation, not behavior.

else:
self.assertIsNotNone(self.executor._task)

async def test_close_sets_stopped(self):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here.

self.executor = _make_executor(target=target)
self.executor.open()
if _IS_SYNC:
self.assertTrue(ran.wait(timeout=2), "target never ran")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this needed if we're calling join() with the same timeout immediately after? We could check that ran is set after the join, which would mean that the executor stopped running or else the join would timeout.

else:
await asyncio.wait_for(ran.wait(), timeout=2)
await self.executor.join(timeout=2)
self.assertTrue(self.executor._stopped)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can remove this if my above comment applies.


self.executor = _make_executor(target=target)
self.executor.open()
self.assertTrue(ran.wait(timeout=2), "target never ran")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here as above.


self.executor = _make_executor(target=target)
self.executor.open()
await asyncio.wait_for(ran.wait(), timeout=2)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

And here.

call_times = []

async def target():
call_times.append(time.monotonic() if _IS_SYNC else asyncio.get_running_loop().time())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why use asyncio.get_running_loop().time() over time.monotonic()? Asyncio's default event loop returns time.monotonic() anyway: https://github.com/python/cpython/blob/b8ebd078f90007d48fcab85effadb33769cd080c/Lib/asyncio/base_events.py#L771.

self.assertGreaterEqual(called[0], 2)


class TestShouldStop(AsyncUnitTest):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Both of these tests are again testing implementation, not behavior. Do they add confidence in the behavior of PeriodicExecutor when used?

self.assertTrue(executor._thread_will_exit)


class TestRegisterExecutor(AsyncUnitTest):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does this test meaningfully add coverage that our existingtest_cleanup_executors_on_client_del in test/test_monitor.py does not?

)


class AsyncPeriodicExecutorTestBase(AsyncUnitTest):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Every test except for test_join_without_open_is_safe now explicitly calls _make_executor() and .join() as part of execution, making this base class largely unused.

finally:
threading.excepthook = orig_excepthook
self.assertEqual(len(captured_exc), 1)
self.assertIsInstance(captured_exc[0], RuntimeError)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we instead check that target is called only once due to an exception being thrown? That would be equivalent here and require less code.

self.assertLess(call_times[1] - call_times[0], 5.0)

async def test_wake_causes_early_run(self):
call_count = [0]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This can be a plain integer if we use nonlocal in target.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

TIL nonlocal exists.

Comment thread test/asynchronous/test_periodic_executor.py Outdated
Comment thread test/asynchronous/test_periodic_executor.py Outdated
await self.executor.join(timeout=3)
self.assertGreaterEqual(call_count[0], 2)

async def test_open_after_target_returns_false(self):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here as above, prefer nonlocal over the list trick.

@aclark4life aclark4life force-pushed the PYTHON-5784 branch 5 times, most recently from b6846db to 5dbfb54 Compare May 5, 2026 22:03
@aclark4life aclark4life requested a review from NoahStapp May 6, 2026 02:09
@aclark4life aclark4life marked this pull request as draft May 6, 2026 18:23
…cated API, gc-safe weakref assertion, non-blocking shutdown test, retrieve task exception
…ous/ via synchro

- Create test/asynchronous/test_periodic_executor.py as the single source of
  truth for all periodic executor tests, using AsyncUnitTest with asyncSetUp/
  asyncTearDown base class for executor lifecycle management
- Register test_periodic_executor.py in synchro's converted_tests so the sync
  variant is auto-generated
- Replace the manually-maintained test/test_periodic_executor.py with the
  synchro-generated equivalent, eliminating duplicated async/sync test code
- Use _IS_SYNC branching for the small number of tests that differ between
  threading (PeriodicExecutor) and asyncio (AsyncPeriodicExecutor) behavior
- Replace pe_module alias with periodic_executor
- Remove underscore from base test class name
- Alpha sort test_periodic_executor.py in synchro list
- Rename ex to executor in tests
- Rename r to executor_repr in repr test
- Replace boom with error in exception messages
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread test/test_periodic_executor.py
Comment thread test/asynchronous/test_periodic_executor.py Outdated
@aclark4life aclark4life force-pushed the PYTHON-5784 branch 2 times, most recently from 6fa5365 to 39cf1f4 Compare May 6, 2026 22:15
@aclark4life aclark4life requested a review from Copilot May 7, 2026 02:02
@aclark4life aclark4life changed the title PYTHON-5784 Increase code coverage for periodic_executor.py PYTHON-5784 Improve code coverage for periodic_executor.py May 7, 2026
@aclark4life aclark4life changed the title PYTHON-5784 Improve code coverage for periodic_executor.py PYTHON-5784 Improve coverage for periodic_executor.py May 7, 2026
@aclark4life aclark4life changed the title PYTHON-5784 Improve coverage for periodic_executor.py PYTHON-5784 Improve coverage for periodic_executor.py May 7, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread test/asynchronous/test_periodic_executor.py Outdated
@aclark4life aclark4life changed the title PYTHON-5784 Improve coverage for periodic_executor.py PYTHON-5784 Coverage improvements for periodic_executor.py May 7, 2026
@aclark4life aclark4life changed the title PYTHON-5784 Coverage improvements for periodic_executor.py PYTHON-5784 Coverage improvement for periodic_executor.py May 7, 2026
@aclark4life aclark4life requested a review from Copilot May 7, 2026 02:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread test/test_periodic_executor.py Outdated
@aclark4life aclark4life changed the title PYTHON-5784 Coverage improvement for periodic_executor.py PYTHON-5784 Coverage increase for periodic_executor.py May 7, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

Comment thread tools/synchro.py
Comment on lines 184 to 194
def async_only_test(f: str) -> bool:
"""Return True for async tests that should not be converted to sync."""
return f in [
"test_locks.py",
"test_concurrency.py",
"test_async_cancellation.py",
"test_async_loop_safety.py",
"test_async_contextvars_reset.py",
"test_async_loop_unblocked.py",
"test_async_periodic_executor.py",
]

sys.path[0:0] = [""]

from test.synchronous import UnitTest, unittest

from pymongo.periodic_executor import PeriodicExecutor

_IS_SYNC = False
_IS_SYNC = False


class TestAsyncPeriodicExecutor(UnitTest):
Comment on lines +57 to +71
def test_target_returning_false_stops_executor(self):
if _IS_SYNC:
ran = threading.Event()
else:
ran = asyncio.Event()

def target():
ran.set()
return False

executor = self._make_executor(target=target)
executor.open()
executor.join(timeout=2)
self.assertTrue(ran.is_set(), "target never ran")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants