Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test for update with IN condition #141

Merged
merged 2 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
implementation: [
{flavor: "other", name: "dynalite"},
{flavor: "dynalite", name: "dynalite"},
{flavor: "other", name: "dynamodb-local"},
{flavor: "other", name: "localstack"},
{flavor: "scylla", name: "scylla"}
Expand Down
6 changes: 3 additions & 3 deletions docs/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Please ensure you have `pre-commit`_ set up so that code formatting is applied a
Tests
-----

To run the tests run ``poetry run pytest``.
To run the tests run ``poetry run pytest``. On most systems ``poetry run pytest --numprocesses auto`` will lead to a much faster execution of the test suite.

Integration Tests
-----------------
Expand All @@ -22,9 +22,9 @@ Alternative DynamoDB Implementations

Currently, aiodynamo is tested with `dynamodb-local`_, `dynalite`_ and `ScyllaDB Alternator`_.

To test with one or more implementations, set the ``DYNAMODB_URLS`` environment variable. The value of that variable should be a space separated list of ``<name>=<config>`` pairs, where ``<config>`` is ``<url>[,<flavor>]`` with ``<flavor>`` being one of ``real``, ``scylla`` or ``other``. The flavor must be set for `ScyllaDB Alternator`_ as it has a slightly different behavior in ``DescribeTable`` compared to other implementations.
To test with one or more implementations, set the ``DYNAMODB_URLS`` environment variable. The value of that variable should be a space separated list of ``<name>=<config>`` pairs, where ``<config>`` is ``<url>[,<flavor>]`` with ``<flavor>`` being one of ``real``, ``dynalite``, ``scylla`` or ``other``. The flavor must be set for `ScyllaDB Alternator`_ as it has a slightly different behavior in ``DescribeTable`` compared to other implementations and `dynalite`_ as it has some known issues.

For example, to run the tests for all three instances with `dynamodb-local`_ running on port 8001, `dynalite`_ running on port 8002 and `ScyllaDB Alternator`_ running on port 8003, you would set ``DYNAMODB_URLS='dynamodb-local=http://localhost:8001 dynalite=http://localhost:8002 scylla=http://localhost:8003,scylla'``
For example, to run the tests for all three instances with `dynamodb-local`_ running on port 8001, `dynalite`_ running on port 8002 and `ScyllaDB Alternator`_ running on port 8003, you would set ``DYNAMODB_URLS='dynamodb-local=http://localhost:8001 dynalite=http://localhost:8002,dynalite scylla=http://localhost:8003,scylla'``

Since these alternative implementations still require credentials to be set, set both ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` to some made up value.

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ httpx = ["httpx"]
aiohttp = ["aiohttp"]

[tool.poetry.group.dev.dependencies]
pytest = "^6.0"
pytest = "^7.0"
pytest-asyncio = "^0.17"
pytest-cov = "^2.6"
black = "^22.3"
Expand All @@ -49,6 +49,7 @@ furo = "^2023.9.10"
ruff = "^0.0.292"
httpx = ">=0.15.0 <1.0.0"
aiohttp = "^3.6.2"
pytest-xdist = "^3.6.1"

[tool.pytest.ini_options]
asyncio_mode = "auto"
Expand Down
19 changes: 15 additions & 4 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
class Flavor(Enum):
real = "real"
scylla = "scylla"
dynalite = "dynalite"
other = "other"


Expand Down Expand Up @@ -94,13 +95,23 @@ def table_name_prefix() -> str:


@pytest.fixture(scope="session")
def real_dynamo(dynamodb_implementation: Implementation) -> bool:
return dynamodb_implementation.flavor is Flavor.real
def flavor(dynamodb_implementation: Implementation) -> Flavor:
return dynamodb_implementation.flavor


@pytest.fixture(scope="session")
def scylla(dynamodb_implementation: Implementation) -> bool:
return dynamodb_implementation.flavor is Flavor.scylla
def real_dynamo(flavor: Flavor) -> bool:
return flavor is Flavor.real


@pytest.fixture(scope="session")
def scylla(flavor: Flavor) -> bool:
return flavor is Flavor.scylla


@pytest.fixture(scope="session")
def dynalite(flavor: Flavor) -> bool:
return flavor is Flavor.dynalite


@pytest.fixture()
Expand Down
47 changes: 46 additions & 1 deletion tests/integration/test_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import asyncio
import contextlib
import secrets
import typing
from operator import itemgetter
from typing import List, Type
from typing import Any, List, Type

import pytest
from yarl import URL
Expand All @@ -10,6 +12,7 @@
from aiodynamo.client import Client
from aiodynamo.credentials import ChainCredentials
from aiodynamo.errors import (
ConditionalCheckFailed,
ItemNotFound,
NoCredentialsFound,
TableNotFound,
Expand Down Expand Up @@ -220,6 +223,48 @@ async def test_update_item(client: Client, table: TableName) -> None:
}


@pytest.mark.parametrize(
"check,cond,ok",
[
(False, F("check").equals(False), True),
(False, F("check").is_in([False]), True),
(0, F("check").is_in([0]), True),
(None, F("check").is_in([None]), True),
(False, F("check").equals(True), False),
(False, F("check").is_in([True]), False),
],
ids=repr,
)
async def test_update_item_condition(
client: Client,
table: TableName,
check: Any,
cond: Condition,
ok: bool,
dynalite: bool,
) -> None:
if dynalite:
pytest.xfail(
"IN condition known to be broken on dynalite: https://github.com/architect/dynalite/pull/159"
)
key = {"h": "hkv", "r": "rkv"}
item = {**key, "target": 1, "check": check}
await client.put_item(table, item)
ctx: typing.Any = (
contextlib.nullcontext() if ok else pytest.raises(ConditionalCheckFailed)
)
with ctx:
updated = await client.update_item(
table,
key,
F("target").add(1),
condition=cond,
return_values=ReturnValues.all_new,
)
assert updated
assert updated["target"] == 2


async def test_delete_item(client: Client, table: TableName) -> None:
item = {"h": "h", "r": "r"}
await client.put_item(table, item)
Expand Down
38 changes: 34 additions & 4 deletions tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,49 @@ def test_binary_decode() -> None:
}


class XDistReprFix:
"""
This wrapper is needed because some types, such as
`DYNAMODB_CONTEXT.create_decimal` cannot be used in
`pytest.mark.parametrize` because they do not have a
stable `__repr__` (the `__repr__` includes the objects
id which is different in each process), causing
non-deterministic test case order, which pytest-xdist
rejects.
"""

def __init__(self, ntc: NumericTypeConverter) -> None:
self.ntc = ntc
self.name = ntc.__name__

def __repr__(self) -> str:
return self.name

def __call__(self, value: str) -> Any:
return self.ntc(value)


@pytest.mark.parametrize(
"value,numeric_type,result",
[
(
{
"N": "1.2",
},
float,
XDistReprFix(float),
1.2,
),
({"NS": ["1.2"]}, float, {1.2}),
({"N": "1.2"}, DYNAMODB_CONTEXT.create_decimal, Decimal("1.2")),
({"NS": ["1.2"]}, DYNAMODB_CONTEXT.create_decimal, {Decimal("1.2")}),
({"NS": ["1.2"]}, XDistReprFix(float), {1.2}),
(
{"N": "1.2"},
XDistReprFix(DYNAMODB_CONTEXT.create_decimal),
Decimal("1.2"),
),
(
{"NS": ["1.2"]},
XDistReprFix(DYNAMODB_CONTEXT.create_decimal),
{Decimal("1.2")},
),
],
)
def test_numeric_decode(
Expand Down
Loading