Skip to content

Commit 6505d3d

Browse files
feat: add version testing for all distributed stores (#125)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: William Easton <[email protected]>
1 parent b3a84a6 commit 6505d3d

File tree

16 files changed

+191
-100
lines changed

16 files changed

+191
-100
lines changed

key-value/key-value-aio/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ addopts = [
5656
"--inline-snapshot=disable",
5757
"-n=auto",
5858
"--dist=loadfile",
59+
"--maxfail=5"
5960
]
6061
markers = [
6162
"skip_on_ci: Skip running the test when running on CI",

key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from typing import TYPE_CHECKING, Any, overload
1+
from datetime import datetime # noqa: TC003
2+
from typing import Any, overload
23

4+
from elastic_transport import ObjectApiResponse # noqa: TC002
35
from key_value.shared.utils.compound import compound_key
46
from key_value.shared.utils.managed_entry import ManagedEntry, load_from_json
57
from key_value.shared.utils.sanitize import (
@@ -33,10 +35,6 @@
3335
msg = "ElasticsearchStore requires py-key-value-aio[elasticsearch]"
3436
raise ImportError(msg) from e
3537

36-
if TYPE_CHECKING:
37-
from datetime import datetime
38-
39-
from elastic_transport import ObjectApiResponse
4038

4139
DEFAULT_INDEX_PREFIX = "kv_store"
4240

key-value/key-value-aio/tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def get_docker_client() -> DockerClient:
3333
return DockerClient.from_env()
3434

3535

36-
@pytest.fixture(scope="session")
36+
@pytest.fixture
3737
def docker_client() -> DockerClient:
3838
return get_docker_client()
3939

key-value/key-value-aio/tests/stores/dynamodb/test_dynamodb.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818

1919
WAIT_FOR_DYNAMODB_TIMEOUT = 30
2020

21+
DYNAMODB_VERSIONS_TO_TEST = [
22+
"2.0.0", # Released Jul 2023
23+
"3.1.0", # Released Sep 2025
24+
]
25+
26+
DYNAMODB_CONTAINER_PORT = 8000
27+
2128

2229
async def ping_dynamodb() -> bool:
2330
"""Check if DynamoDB Local is running."""
@@ -43,16 +50,18 @@ class DynamoDBFailedToStartError(Exception):
4350

4451
@pytest.mark.skipif(should_skip_docker_tests(), reason="Docker is not available")
4552
class TestDynamoDBStore(ContextManagerStoreTestMixin, BaseStoreTests):
46-
@pytest.fixture(autouse=True, scope="session")
47-
async def setup_dynamodb(self) -> AsyncGenerator[None, None]:
53+
@pytest.fixture(autouse=True, scope="session", params=DYNAMODB_VERSIONS_TO_TEST)
54+
async def setup_dynamodb(self, request: pytest.FixtureRequest) -> AsyncGenerator[None, None]:
55+
version = request.param
56+
4857
# DynamoDB Local container
4958
with docker_container(
50-
"dynamodb-test",
51-
"amazon/dynamodb-local:latest",
52-
{"8000": DYNAMODB_HOST_PORT},
59+
f"dynamodb-test-{version}",
60+
f"amazon/dynamodb-local:{version}",
61+
{str(DYNAMODB_CONTAINER_PORT): DYNAMODB_HOST_PORT},
5362
):
54-
if not await async_wait_for_true(bool_fn=ping_dynamodb, tries=30, wait_time=1):
55-
msg = "DynamoDB failed to start"
63+
if not await async_wait_for_true(bool_fn=ping_dynamodb, tries=WAIT_FOR_DYNAMODB_TIMEOUT, wait_time=1):
64+
msg = f"DynamoDB {version} failed to start"
5665
raise DynamoDBFailedToStartError(msg)
5766

5867
yield

key-value/key-value-aio/tests/stores/elasticsearch/test_elasticsearch.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import os
21
from collections.abc import AsyncGenerator
32

43
import pytest
@@ -8,15 +7,21 @@
87

98
from key_value.aio.stores.base import BaseStore
109
from key_value.aio.stores.elasticsearch import ElasticsearchStore
11-
from tests.conftest import docker_container
10+
from tests.conftest import docker_container, should_skip_docker_tests
1211
from tests.stores.base import BaseStoreTests, ContextManagerStoreTestMixin
1312

1413
TEST_SIZE_LIMIT = 1 * 1024 * 1024 # 1MB
1514
ES_HOST = "localhost"
1615
ES_PORT = 9200
1716
ES_URL = f"http://{ES_HOST}:{ES_PORT}"
18-
ES_VERSION = "9.1.4"
19-
ES_IMAGE = f"docker.elastic.co/elasticsearch/elasticsearch:{ES_VERSION}"
17+
ES_CONTAINER_PORT = 9200
18+
19+
WAIT_FOR_ELASTICSEARCH_TIMEOUT = 30
20+
21+
ELASTICSEARCH_VERSIONS_TO_TEST = [
22+
"9.0.0", # Released Apr 2025
23+
"9.2.0", # Released Oct 2025
24+
]
2025

2126

2227
def get_elasticsearch_client() -> AsyncElasticsearch:
@@ -34,15 +39,21 @@ class ElasticsearchFailedToStartError(Exception):
3439
pass
3540

3641

37-
@pytest.mark.skipif(os.getenv("ES_URL") is None, reason="Elasticsearch is not configured")
42+
@pytest.mark.skipif(should_skip_docker_tests(), reason="Docker is not running")
3843
class TestElasticsearchStore(ContextManagerStoreTestMixin, BaseStoreTests):
39-
@pytest.fixture(autouse=True, scope="session")
40-
async def setup_elasticsearch(self) -> AsyncGenerator[None, None]:
44+
@pytest.fixture(autouse=True, scope="session", params=ELASTICSEARCH_VERSIONS_TO_TEST)
45+
async def setup_elasticsearch(self, request: pytest.FixtureRequest) -> AsyncGenerator[None, None]:
46+
version = request.param
47+
es_image = f"docker.elastic.co/elasticsearch/elasticsearch:{version}"
48+
4149
with docker_container(
42-
"elasticsearch-test", ES_IMAGE, {"9200": ES_PORT}, {"discovery.type": "single-node", "xpack.security.enabled": "false"}
50+
f"elasticsearch-test-{version}",
51+
es_image,
52+
{str(ES_CONTAINER_PORT): ES_PORT},
53+
{"discovery.type": "single-node", "xpack.security.enabled": "false"},
4354
):
44-
if not await async_wait_for_true(bool_fn=ping_elasticsearch, tries=30, wait_time=2):
45-
msg = "Elasticsearch failed to start"
55+
if not await async_wait_for_true(bool_fn=ping_elasticsearch, tries=WAIT_FOR_ELASTICSEARCH_TIMEOUT, wait_time=2):
56+
msg = f"Elasticsearch {version} failed to start"
4657
raise ElasticsearchFailedToStartError(msg)
4758

4859
yield
@@ -55,10 +66,10 @@ async def es_client(self) -> AsyncGenerator[AsyncElasticsearch, None]:
5566
@override
5667
@pytest.fixture
5768
async def store(self) -> AsyncGenerator[ElasticsearchStore, None]:
58-
es_client = get_elasticsearch_client()
59-
indices = await es_client.options(ignore_status=404).indices.get(index="kv-store-e2e-test-*")
60-
for index in indices:
61-
_ = await es_client.options(ignore_status=404).indices.delete(index=index)
69+
async with get_elasticsearch_client() as es_client:
70+
indices = await es_client.options(ignore_status=404).indices.get(index="kv-store-e2e-test-*")
71+
for index in indices:
72+
_ = await es_client.options(ignore_status=404).indices.delete(index=index)
6273
async with ElasticsearchStore(url=ES_URL, index_prefix="kv-store-e2e-test") as store:
6374
yield store
6475

key-value/key-value-aio/tests/stores/memcached/test_memcached.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
# Memcached test configuration
1515
MEMCACHED_HOST = "localhost"
1616
MEMCACHED_PORT = 11211
17-
MEMCACHED_IMAGE = "memcached:1.6"
17+
MEMCACHED_CONTAINER_PORT = 11211
1818

1919
WAIT_FOR_MEMCACHED_TIMEOUT = 30
2020

21+
MEMCACHED_VERSIONS_TO_TEST = [
22+
"1.6.0-alpine", # Released Mar 2020
23+
"1.6.39-alpine", # Released Sep 2025
24+
]
25+
2126

2227
async def ping_memcached() -> bool:
2328
client = Client(host=MEMCACHED_HOST, port=MEMCACHED_PORT)
@@ -38,18 +43,20 @@ class MemcachedFailedToStartError(Exception):
3843

3944
@pytest.mark.skipif(should_skip_docker_tests(), reason="Docker is not available")
4045
class TestMemcachedStore(ContextManagerStoreTestMixin, BaseStoreTests):
41-
@pytest.fixture(autouse=True, scope="session")
42-
async def setup_memcached(self) -> AsyncGenerator[None, None]:
43-
with docker_container("memcached-test", MEMCACHED_IMAGE, {"11211": MEMCACHED_PORT}):
44-
if not await async_wait_for_true(bool_fn=ping_memcached, tries=30, wait_time=1):
45-
msg = "Memcached failed to start"
46+
@pytest.fixture(autouse=True, scope="session", params=MEMCACHED_VERSIONS_TO_TEST)
47+
async def setup_memcached(self, request: pytest.FixtureRequest) -> AsyncGenerator[None, None]:
48+
version = request.param
49+
50+
with docker_container(f"memcached-test-{version}", f"memcached:{version}", {str(MEMCACHED_CONTAINER_PORT): MEMCACHED_PORT}):
51+
if not await async_wait_for_true(bool_fn=ping_memcached, tries=WAIT_FOR_MEMCACHED_TIMEOUT, wait_time=1):
52+
msg = f"Memcached {version} failed to start"
4653
raise MemcachedFailedToStartError(msg)
4754

4855
yield
4956

5057
@override
5158
@pytest.fixture
52-
async def store(self, setup_memcached: MemcachedStore) -> MemcachedStore:
59+
async def store(self, setup_memcached: None) -> MemcachedStore:
5360
store = MemcachedStore(host=MEMCACHED_HOST, port=MEMCACHED_PORT)
5461
_ = await store._client.flush_all() # pyright: ignore[reportPrivateUsage]
5562
return store

key-value/key-value-aio/tests/stores/mongodb/test_mongodb.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020

2121
WAIT_FOR_MONGODB_TIMEOUT = 30
2222

23+
MONGODB_VERSIONS_TO_TEST = [
24+
"5.0", # Older supported version
25+
"8.0", # Latest stable version
26+
]
27+
2328

2429
async def ping_mongodb() -> bool:
2530
try:
@@ -37,11 +42,13 @@ class MongoDBFailedToStartError(Exception):
3742

3843
@pytest.mark.skipif(should_skip_docker_tests(), reason="Docker is not available")
3944
class TestMongoDBStore(ContextManagerStoreTestMixin, BaseStoreTests):
40-
@pytest.fixture(autouse=True, scope="session")
41-
async def setup_mongodb(self) -> AsyncGenerator[None, None]:
42-
with docker_container("mongodb-test", "mongo:7", {"27017": 27017}):
43-
if not await async_wait_for_true(bool_fn=ping_mongodb, tries=30, wait_time=1):
44-
msg = "MongoDB failed to start"
45+
@pytest.fixture(autouse=True, scope="session", params=MONGODB_VERSIONS_TO_TEST)
46+
async def setup_mongodb(self, request: pytest.FixtureRequest) -> AsyncGenerator[None, None]:
47+
version = request.param
48+
49+
with docker_container(f"mongodb-test-{version}", f"mongo:{version}", {str(MONGODB_HOST_PORT): MONGODB_HOST_PORT}):
50+
if not await async_wait_for_true(bool_fn=ping_mongodb, tries=WAIT_FOR_MONGODB_TIMEOUT, wait_time=1):
51+
msg = f"MongoDB {version} failed to start"
4552
raise MongoDBFailedToStartError(msg)
4653

4754
yield

key-value/key-value-aio/tests/stores/valkey/test_valkey.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
from collections.abc import AsyncGenerator
23

34
import pytest
@@ -15,9 +16,16 @@
1516
VALKEY_HOST = "localhost"
1617
VALKEY_PORT = 6380 # normally 6379, avoid clashing with Redis tests
1718
VALKEY_DB = 15
19+
VALKEY_CONTAINER_PORT = 6379
1820

1921
WAIT_FOR_VALKEY_TIMEOUT = 30
2022

23+
VALKEY_VERSIONS_TO_TEST = [
24+
"7.2.5", # Released Apr 2024
25+
"8.0.0", # Released Sep 2024
26+
"9.0.0", # Released Oct 2025
27+
]
28+
2129

2230
class ValkeyFailedToStartError(Exception):
2331
pass
@@ -36,19 +44,26 @@ async def get_valkey_client(self):
3644
return await GlideClient.create(config=client_config)
3745

3846
async def ping_valkey(self) -> bool:
47+
client = None
3948
try:
4049
client = await self.get_valkey_client()
41-
_ = await client.ping()
50+
await client.ping()
4251
except Exception:
4352
return False
44-
45-
return True
46-
47-
@pytest.fixture(scope="session")
48-
async def setup_valkey(self) -> AsyncGenerator[None, None]:
49-
with docker_container("valkey-test", "valkey/valkey:latest", {"6379": VALKEY_PORT}):
50-
if not await async_wait_for_true(bool_fn=self.ping_valkey, tries=30, wait_time=1):
51-
msg = "Valkey failed to start"
53+
else:
54+
return True
55+
finally:
56+
if client is not None:
57+
with contextlib.suppress(Exception):
58+
await client.close()
59+
60+
@pytest.fixture(scope="session", params=VALKEY_VERSIONS_TO_TEST)
61+
async def setup_valkey(self, request: pytest.FixtureRequest) -> AsyncGenerator[None, None]:
62+
version = request.param
63+
64+
with docker_container(f"valkey-test-{version}", f"valkey/valkey:{version}", {str(VALKEY_CONTAINER_PORT): VALKEY_PORT}):
65+
if not await async_wait_for_true(bool_fn=self.ping_valkey, tries=WAIT_FOR_VALKEY_TIMEOUT, wait_time=1):
66+
msg = f"Valkey {version} failed to start"
5267
raise ValkeyFailedToStartError(msg)
5368

5469
yield

key-value/key-value-aio/tests/stores/vault/test_vault.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
VAULT_PORT = 8200
1616
VAULT_TOKEN = "dev-root-token" # noqa: S105
1717
VAULT_MOUNT_POINT = "secret"
18+
VAULT_CONTAINER_PORT = 8200
19+
20+
WAIT_FOR_VAULT_TIMEOUT = 30
21+
22+
VAULT_VERSIONS_TO_TEST = [
23+
"1.12.0", # Released Oct 2022
24+
"1.21.0", # Released Oct 2025
25+
]
1826

1927

2028
class VaultFailedToStartError(Exception):
@@ -23,31 +31,33 @@ class VaultFailedToStartError(Exception):
2331

2432
@pytest.mark.skipif(should_skip_docker_tests(), reason="Docker is not running")
2533
class TestVaultStore(BaseStoreTests):
26-
async def get_vault_client(self):
34+
def get_vault_client(self):
2735
import hvac
2836

2937
return hvac.Client(url=f"http://{VAULT_HOST}:{VAULT_PORT}", token=VAULT_TOKEN)
3038

3139
async def ping_vault(self) -> bool:
3240
try:
33-
client = await self.get_vault_client()
41+
client = self.get_vault_client()
3442
return client.sys.is_initialized() # pyright: ignore[reportUnknownMemberType,reportUnknownReturnType,reportUnknownVariableType]
3543
except Exception:
3644
return False
3745

38-
@pytest.fixture(scope="session")
39-
async def setup_vault(self) -> AsyncGenerator[None, None]:
46+
@pytest.fixture(scope="session", params=VAULT_VERSIONS_TO_TEST)
47+
async def setup_vault(self, request: pytest.FixtureRequest) -> AsyncGenerator[None, None]:
48+
version = request.param
49+
4050
with docker_container(
41-
"vault-test",
42-
"hashicorp/vault:latest",
43-
{"8200": VAULT_PORT},
51+
f"vault-test-{version}",
52+
f"hashicorp/vault:{version}",
53+
{str(VAULT_CONTAINER_PORT): VAULT_PORT},
4454
environment={
4555
"VAULT_DEV_ROOT_TOKEN_ID": VAULT_TOKEN,
4656
"VAULT_DEV_LISTEN_ADDRESS": "0.0.0.0:8200",
4757
},
4858
):
49-
if not await async_wait_for_true(bool_fn=self.ping_vault, tries=30, wait_time=1):
50-
msg = "Vault failed to start"
59+
if not await async_wait_for_true(bool_fn=self.ping_vault, tries=WAIT_FOR_VAULT_TIMEOUT, wait_time=1):
60+
msg = f"Vault {version} failed to start"
5161
raise VaultFailedToStartError(msg)
5262

5363
yield
@@ -64,7 +74,7 @@ async def store(self, setup_vault: None):
6474
)
6575

6676
# Clean up any existing data - best effort, ignore errors
67-
client = await self.get_vault_client()
77+
client = self.get_vault_client()
6878
try:
6979
# List all secrets and delete them
7080
secrets_list = client.secrets.kv.v2.list_secrets(path="", mount_point=VAULT_MOUNT_POINT) # pyright: ignore[reportUnknownMemberType,reportUnknownReturnType,reportUnknownVariableType]

key-value/key-value-sync/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ addopts = [
5555
"--inline-snapshot=disable",
5656
"-n=auto",
5757
"--dist=loadfile",
58+
"--maxfail=5"
5859
]
5960
markers = [
6061
"skip_on_ci: Skip running the test when running on CI",

0 commit comments

Comments
 (0)