diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..bb7a669 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,40 @@ +name: Python package + +on: + push: + branches: [ master, chore/updates ] + pull_request: + branches: [ master ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v6.0.3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6.2.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + + - name: Run tests with tox + run: | + tox -e py + + - name: Run style checks with tox (Python 3.12 only) + if: ${{ matrix.python-version == '3.12' }} + run: | + tox -e codestyle diff --git a/README.md b/README.md index 2685a86..248c7ba 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,25 @@ `STORAGE_CONFIG` will send any option specified as an argument to `redis.StrictRedis`. 3. Start your bot in text mode: `errbot -T` to give it a shot. + +### Running Tests + +To run the automated test suite: + +1. Create and activate a Python virtual environment: + ```bash + python3 -m venv .venv + source .venv/bin/activate + ``` +2. Install the dependencies and testing requirements: + ```bash + pip install -r requirements.txt errbot pytest tox fakeredis + ``` +3. Run the tests using `pytest` directly: + ```bash + pytest + ``` + Or run both tests and style checks using `tox`: + ```bash + tox + ``` diff --git a/redisstorage.py b/redisstorage.py index 0895fde..4946325 100644 --- a/redisstorage.py +++ b/redisstorage.py @@ -41,7 +41,7 @@ def get(self, key: str) -> Any: result = self.redis.get(unique_key) if result is None: raise KeyError("%s doesn't exists." % (unique_key)) - return decode(result.decode()) + return decode(result.decode(), keys=True) def remove(self, key: str): unique_key = self._make_nskey(key) @@ -52,8 +52,8 @@ def remove(self, key: str): def set(self, key: str, value: Any) -> None: unique_key = self._make_nskey(key) - log.debug("Setting value '%s' at '%s'", encode(value), unique_key) - self.redis.set(unique_key, encode(value)) + log.debug("Setting value '%s' at '%s'", encode(value, keys=True), unique_key) + self.redis.set(unique_key, encode(value, keys=True)) def len(self): return len(self.keys()) diff --git a/requirements.txt b/requirements.txt index 88693a5..426b3b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -jsonpickle -redis +jsonpickle>=3.0.0 +redis>=5.0.0 diff --git a/test_redis.py b/test_redis.py new file mode 100644 index 0000000..e014d5a --- /dev/null +++ b/test_redis.py @@ -0,0 +1,117 @@ +import logging + +import fakeredis +import pytest + +import redisstorage + + +class MockBotConfig: + def __init__(self, storage_config=None): + self.STORAGE_CONFIG = storage_config if storage_config is not None else {} + self.BOT_LOG_LEVEL = logging.WARNING + + +def test_storage_operations(monkeypatch): + fake_redis = fakeredis.FakeStrictRedis() + monkeypatch.setattr(redisstorage.redis, "StrictRedis", lambda **kwargs: fake_redis) + + config = MockBotConfig() + plugin = redisstorage.RedisPlugin(config) + storage = plugin.open("test_namespace") + + # Initial state + assert storage.len() == 0 + assert list(storage.keys()) == [] + + # Get non-existent + with pytest.raises(KeyError): + storage.get("key1") + + # Set and get basic values + storage.set("key1", "value1") + assert storage.len() == 1 + assert storage.get("key1") == "value1" + assert list(storage.keys()) == ["key1"] + + # Set and get complex objects (list, dict, int) + storage.set("key2", {"a": 1, "b": [2, 3]}) + assert storage.len() == 2 + assert storage.get("key2") == {"a": 1, "b": [2, 3]} + assert set(storage.keys()) == {"key1", "key2"} + + # Overwrite value + storage.set("key1", "new_value1") + assert storage.get("key1") == "new_value1" + + # Remove non-existent key + with pytest.raises(KeyError): + storage.remove("nonexistent") + + # Remove existing key + storage.remove("key1") + assert storage.len() == 1 + with pytest.raises(KeyError): + storage.get("key1") + + assert list(storage.keys()) == ["key2"] + + # Close storage + storage.close() + + +def test_multiple_namespaces(monkeypatch): + fake_redis = fakeredis.FakeStrictRedis() + monkeypatch.setattr(redisstorage.redis, "StrictRedis", lambda **kwargs: fake_redis) + + config = MockBotConfig() + plugin = redisstorage.RedisPlugin(config) + + storage_a = plugin.open("namespace_a") + storage_b = plugin.open("namespace_b") + + storage_a.set("shared_key", "value_a") + storage_b.set("shared_key", "value_b") + + assert storage_a.get("shared_key") == "value_a" + assert storage_b.get("shared_key") == "value_b" + + assert storage_a.len() == 1 + assert storage_b.len() == 1 + + storage_a.close() + storage_b.close() + + +def test_simple_store_retrieve(monkeypatch): + from errbot.storage import StoreMixin + + fake_redis = fakeredis.FakeStrictRedis() + monkeypatch.setattr(redisstorage.redis, "StrictRedis", lambda **kwargs: fake_redis) + + config = MockBotConfig() + plugin = redisstorage.RedisPlugin(config) + sm = StoreMixin() + sm.open_storage(plugin, "ns") + sm["toto"] = "titui" + assert sm["toto"] == "titui" + sm.close_storage() + + +def test_mutable(monkeypatch): + from errbot.storage import StoreMixin + + fake_redis = fakeredis.FakeStrictRedis() + monkeypatch.setattr(redisstorage.redis, "StrictRedis", lambda **kwargs: fake_redis) + + config = MockBotConfig() + plugin = redisstorage.RedisPlugin(config) + sm = StoreMixin() + sm.open_storage(plugin, "ns") + sm["toto"] = [1, 3] + + with sm.mutable("toto") as titi: + titi[1] = 5 + + assert sm["toto"] == [1, 5] + sm.close_storage() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..d0ee327 --- /dev/null +++ b/tox.ini @@ -0,0 +1,21 @@ +[tox] +envlist = py, codestyle +skipsdist = True + +[testenv] +deps = + pytest + jsonpickle>=3.0.0 + redis + fakeredis + errbot +commands = pytest -v + +[testenv:codestyle] +deps = + ruff + isort +commands = + ruff check redisstorage.py test_redis.py + ruff format --check redisstorage.py test_redis.py + isort --check-only redisstorage.py test_redis.py