From 885b4b2510e4527f63a2dffba02506bc7e18f590 Mon Sep 17 00:00:00 2001 From: Sylvain MOUQUET Date: Sun, 20 Oct 2024 21:21:28 +0200 Subject: [PATCH] feat(containers): add redis (#15) --- README.md | 2 +- pydocks/__init__.py | 9 ++++ pydocks/plugin.py | 1 - pydocks/redis.py | 100 ++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + test/test_redis.py | 75 +++++++++++++++++++++++++++++++++ uv.lock | 16 +++++++ 7 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 pydocks/redis.py create mode 100644 test/test_redis.py diff --git a/README.md b/README.md index 94257c3..4422a2a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ PyDocks is a Python library that provides a set of pytest fixtures for running t Key features include: - Easy integration with pytest -- Support for PostgreSQL, Hashicorp Vault containers +- Support for PostgreSQL, Hashicorp Vault containers, Redis - Automatic container cleanup - Configurable container settings - Reusable session-scoped containers for improved test performance diff --git a/pydocks/__init__.py b/pydocks/__init__.py index 82c26c2..ed0786d 100644 --- a/pydocks/__init__.py +++ b/pydocks/__init__.py @@ -8,6 +8,9 @@ "vault_clean_all_containers", "vault_container", "vault_container_session", + "redis_clean_all_containers", + "redis_container", + "redis_container_session", ) # pytest_plugins = ["pydocks.conftest"] @@ -23,3 +26,9 @@ vault_container, vault_container_session, ) + +from pydocks.redis import ( + redis_clean_all_containers, + redis_container, + redis_container_session, +) diff --git a/pydocks/plugin.py b/pydocks/plugin.py index 6f733cc..e25db63 100644 --- a/pydocks/plugin.py +++ b/pydocks/plugin.py @@ -15,7 +15,6 @@ @pytest.fixture(scope="session", autouse=True) def docker(): - if "DOCKER_SOCK" in os.environ: yield DockerClient(host=os.environ["DOCKER_SOCK"]) elif "CI" in os.environ: diff --git a/pydocks/redis.py b/pydocks/redis.py new file mode 100644 index 0000000..0d1e47f --- /dev/null +++ b/pydocks/redis.py @@ -0,0 +1,100 @@ +import pytest +import os + + +import pytest_asyncio +from python_on_whales import docker as libdocker +from reattempt import reattempt +import logging +import uuid + +from pydocks.plugin import ( + clean_containers, + socket_test_connection, + wait_and_run_container, +) + + +logger = logging.getLogger("pydocks") +logger.addHandler(logging.NullHandler()) + + +# https://hub.docker.com/_/redis/tags +TEST_REDIS_DOCKER_IMAGE: str = "docker.io/redis:7.4.1" + + +@pytest_asyncio.fixture(scope="session", loop_scope="session") +async def redis_clean_all_containers(docker): + container_name: str = "test-redis" + # clean before + + await clean_containers(docker, container_name) + yield + # clean after + await clean_containers(docker, container_name) + + +@pytest.fixture(scope="function") +async def redis_container(docker: libdocker, mocker): # type: ignore + mocker.patch( + "logging.exception", + lambda *args, **kwargs: logger.warning(f"Exception raised {args}"), + ) + + container_name = f"test-redis-{uuid.uuid4()}" + # optional : await clean_containers(docker, container_name) + + async for container in setup_redis_container(docker, container_name): + yield container + + +@pytest_asyncio.fixture(scope="session", loop_scope="session") +async def redis_container_session(docker: libdocker, session_mocker): # type: ignore + session_mocker.patch( + "logging.exception", + lambda *args, **kwargs: logger.warning(f"Exception raised {args}"), + ) + + await clean_containers(docker, "test-redis-session") + + container_name = f"test-redis-session-{uuid.uuid4()}" + + async for container in setup_redis_container(docker, container_name): + yield container + + +async def setup_redis_container(docker: libdocker, container_name): # type: ignore + redis_image = ( + TEST_REDIS_DOCKER_IMAGE + if "TEST_REDIS_DOCKER_IMAGE" not in os.environ + else os.environ["TEST_REDIS_DOCKER_IMAGE"] + ) + logger.debug(f"pull docker image : {redis_image}") + + def run_container(container_name: str): + return docker.run( + image=redis_image, + name=container_name, + detach=True, + publish=[(6379, 6379)], + expose=[6379], + ) + + # Select the container with the given name if exists, else create a new one + containers = docker.ps(all=True, filters={"name": f"^{container_name}$"}) + if containers and len(containers) > 0: + container = containers[0] # type: ignore + logger.debug(f"found existing container: {container_name}") + else: + logger.debug(f"no existing container found, creating new one: {container_name}") + container = run_container(container_name) + + await redis_test_connection() + + async for instance in wait_and_run_container(docker, container, container_name): + yield instance + + +@reattempt(max_retries=30, min_time=0.1, max_time=0.5) +async def redis_test_connection(): + await socket_test_connection("127.0.0.1", 6379) diff --git a/pyproject.toml b/pyproject.toml index b459086..db308f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ authors = [{name = "Sylvain Mouquet", email = "sylvain.mouquet@gmail.com"}] readme = "README.md" requires-python = ">=3.9" dependencies = [ + "redis>=5.1.1", ] license = { text = "MIT" } url = "https://github.com/sylvainmouquet/pydocks" diff --git a/test/test_redis.py b/test/test_redis.py new file mode 100644 index 0000000..7e9387a --- /dev/null +++ b/test/test_redis.py @@ -0,0 +1,75 @@ +import pytest +import os +import redis.asyncio as redis +from loguru import logger +import pytest_asyncio + + +@pytest_asyncio.fixture(scope="session", loop_scope="session", autouse=True) +async def begin_clean_all_containers(redis_clean_all_containers): + logger.info("Begin - clean all containers") + + +@pytest.mark.asyncio +async def test_redis_default_version(redis_container): + container_env_dict = dict(env.split("=") for env in redis_container.config.env) + + assert container_env_dict["REDIS_VERSION"] == "7.4.1" + + +@pytest.fixture +def custom_redis_version(): + os.environ["TEST_REDIS_DOCKER_IMAGE"] = "docker.io/redis:7.4.0" + yield + del os.environ["TEST_REDIS_DOCKER_IMAGE"] + + +@pytest.mark.asyncio +async def test_redis_custom_version(custom_redis_version, redis_container): + container_env_dict = dict(env.split("=") for env in redis_container.config.env) + + assert container_env_dict["REDIS_VERSION"] == "7.4.0" + + +@pytest.mark.asyncio +async def test_redis_execute_command(redis_container): + # Execute Redis CLI command + result = redis_container.execute(["redis-cli", "PING"]) + assert result.strip() == "PONG" + + # Set a key-value pair + set_result = redis_container.execute(["redis-cli", "SET", "test_key", "test_value"]) + assert set_result.strip() == "OK" + + # Get the value + get_result = redis_container.execute(["redis-cli", "GET", "test_key"]) + assert get_result.strip() == "test_value" + + # Delete the key + del_result = redis_container.execute(["redis-cli", "DEL", "test_key"]) + assert del_result.strip() == "1" + + # Verify key is deleted + get_deleted = redis_container.execute(["redis-cli", "GET", "test_key"]) + assert get_deleted.strip() == "" + + async with await redis.from_url( + "redis://localhost:6379", encoding="utf8" + ) as rredis: + # Flush all existing data + await rredis.flushall() + + # Set a key-value pair + await rredis.set("test_key", "test_value") + + # Get the value + value = await rredis.get("test_key") + assert value == b"test_value" + + # Delete the key + deleted = await rredis.delete("test_key") + assert deleted == 1 + + # Verify key is deleted + deleted_value = await rredis.get("test_key") + assert deleted_value is None diff --git a/uv.lock b/uv.lock index dee7771..afebc43 100644 --- a/uv.lock +++ b/uv.lock @@ -763,6 +763,9 @@ wheels = [ name = "pydocks" version = "1.0.2" source = { editable = "." } +dependencies = [ + { name = "redis" }, +] [package.dev-dependencies] dev = [ @@ -780,6 +783,7 @@ dev = [ ] [package.metadata] +requires-dist = [{ name = "redis", specifier = ">=5.1.1" }] [package.metadata.requires-dev] dev = [ @@ -872,6 +876,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/a9/33afc6273d883a97ab361f6bcb17cd661092ee84e27e2e9560e7b97efcbf/reattempt-1.1.2-py3-none-any.whl", hash = "sha256:3d58075814f149da73e8cca8403d118758d4c7ab5d2d85a988274f02d281e565", size = 5164 }, ] +[[package]] +name = "redis" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/58/dcf97c3c09d429c3bb830d6075322256da3dba42df25359bd1c82b442d20/redis-5.1.1.tar.gz", hash = "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72", size = 4607302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/f1/feeeaaaac0f589bcbc12c02da357cf635ee383c9128b77230a1e99118885/redis-5.1.1-py3-none-any.whl", hash = "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24", size = 261283 }, +] + [[package]] name = "requests" version = "2.32.3"