Skip to content

test: move TTL expiry on a controlled clock instead of sleeping - #288

Merged
allen0099 merged 1 commit into
masterfrom
test/controlled-time-185
Sep 27, 2026
Merged

allen0099 merged 1 commit into
masterfrom
test/controlled-time-185

Conversation

@allen0099

Copy link
Copy Markdown
Owner

Summary

Closes #185

Part of #123. About 22 s of the ~25.5 s suite (without live servers) was spent sleeping until TTLs ran out. The tests now move a clock instead.

The clock fixture (tests/conftest.py)

  • clock points every time read behind a TTL or expiry check at a settable Clock:
    • time.time() in the memory backend;
    • datetime.now() in session.manager, session.models, state.manager and state.models.
  • It patches module references only, and no library code changes.
  • clock.advance(seconds) moves it.
  • await clock.wait(seconds, backend) advances it and also sleeps for real when backend is not the memory backend. It is for the tests parametrized over memory and a live Redis, whose server keeps its own time.

Converted tests

Test Was Now
test_memory.py: ttl_expiry, cleanup, get_evicts_the_expired_entry_it_skips; test_proxybackend.py::test_backend_cleanup 1.05–2 s sleep clock.advance
test_memory.py: cleanup_task_impl, write_only_use_starts_the_cleanup_task 2 s sleep for a 1 s sweep interval clock.advance; the instance's cleanup_interval is set to 10 ms with monkeypatch, and the test polls the dict for up to 1 s of real time
test_cache_manager.py: 6 TTL tests; state/test_manager.py::test_get_state_metadata_after_expire 1.1–1.2 s sleep clock.advance, or clock.wait for the tests also parametrized with Redis
session/test_jwt.py::test_jwt_expiration_enforced 1.2 s sleep PyJWT checks exp against the real time, so the session is created 5 s in the past on the clock. Its backend entry and the session itself are still current on the clock, so only the token check can reject it.
backends/test_memcached.py::test_memcached_tries_a_failed_server_again_after_the_dead_timeout 1.1 s sleep pymemcache times the dead timeout with the real clock, so the test sets _DEAD_TIMEOUT to 0.05 s and sleeps 0.1 s. A separate assertion pins the documented value of 1 s.
session/test_models.py::test_session_renew, session/test_manager.py::test_sliding_expiration 10 ms sleep "to ensure time passes" clock.advance(1). test_session_renew now asserts > instead of >=.

Sleeps that stay

  • The Redis-parametrized variants above, through clock.wait.
  • The live-server tests that rely on real server-side expiry: test_redis.py:316 (1.5 s), and test_memcached.py:460 (2 s) and :790 (2.1 s). Memcached's expiry has one-second resolution.
  • Short sleeps that are not waiting for a TTL:
    • race windows (time.sleep(0.05) in the proxy tests);
    • lock contention (test_lock.py);
    • test_cache_hit.py's slow handler;
    • a 10 ms yield in test_cache_manager.py.

Tests

Runtime (uv run pytest, without live servers): 25.5 s → 3.6 s, with 845 passed and 190 skipped both before and after. The suite also passed three times in random order.

Mutation checks, full suite, with each mutation applied on its own:

Mutation Converted tests that fail
Memory entries never expire (_is_live returns True) all 5 converted memory tests, test_backend_cleanup, and all 6 CacheManager TTL tests (25 tests fail in total)
The sweep removes nothing cleanup_task_impl, write_only_use_starts_the_cleanup_task, test_backend_cleanup
CacheManager ignores default_ttl test_add_uses_default_ttl, test_default_ttl_used_when_not_specified
Session.renew keeps the old expiry test_session_renew
Sliding renewal never happens test_sliding_expiration, plus 7 other sliding tests
JWT verify_exp is False test_jwt_expiration_enforced
The backend does not pass dead_timeout to pymemcache the dead-timeout test
State expiry: memory expiry and _is_past disabled together test_get_state_metadata_after_expire[MemoryBackend]

On the last row: that state test, like before this PR, passes if only one of the two expiry paths is broken, because either one hides the state.

Other checks

  • mypy --strict on the changed test files: clean.
  • pre-commit: passes.
  • uv lock --check: passes.

Live servers were not run by me.

CHANGELOG

None: test-only.

A clock fixture in tests/conftest.py points the memory backend's
time.time() and the session and state modules' datetime.now() at a
settable clock. The TTL tests advance it instead of sleeping; tests that
also run against a live Redis still sleep there, since the server keeps
its own time. The sweeper tests shorten the cleanup interval, the JWT
expiry test creates its session in the past because PyJWT reads the
real time, and the Memcached dead-timeout test shortens _DEAD_TIMEOUT.

The suite without live servers drops from about 25.5 s to 3.6 s.
@allen0099 allen0099 added this to the 0.3.8 milestone Sep 27, 2026
@allen0099
allen0099 merged commit 80c2d0e into master Sep 27, 2026
11 checks passed
@allen0099
allen0099 deleted the test/controlled-time-185 branch September 27, 2026 10:45
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.

Tests: control time instead of sleeping for TTLs

1 participant