WIP on async - test failure due to closed event loop

This commit is contained in:
Andrew Brookins 2021-10-22 06:33:05 -07:00
parent 0f9f7aa868
commit b2c2dd9f6f
22 changed files with 348 additions and 190 deletions

View file

@ -1,19 +1,18 @@
import random
import pytest
from redis import Redis
from redis_developer.connections import get_redis_connection
from redis_om.connections import get_redis_connection
@pytest.fixture
def redis():
def redis(event_loop):
yield get_redis_connection()
def _delete_test_keys(prefix: str, conn: Redis):
async def _delete_test_keys(prefix: str, conn):
keys = []
for key in conn.scan_iter(f"{prefix}:*"):
async for key in conn.scan_iter(f"{prefix}:*"):
keys.append(key)
if keys:
conn.delete(*keys)
@ -21,11 +20,10 @@ def _delete_test_keys(prefix: str, conn: Redis):
@pytest.fixture
def key_prefix(redis):
key_prefix = f"redis-developer:{random.random()}"
key_prefix = f"redis-om:{random.random()}"
yield key_prefix
_delete_test_keys(key_prefix, redis)
@pytest.fixture(autouse=True)
def delete_test_keys(redis, request, key_prefix):
_delete_test_keys(key_prefix, redis)
async def delete_test_keys(redis, request, key_prefix):
await _delete_test_keys(key_prefix, redis)

View file

@ -8,9 +8,9 @@ from unittest import mock
import pytest
from pydantic import ValidationError
from redis_developer.model import Field, HashModel
from redis_developer.model.migrations.migrator import Migrator
from redis_developer.model.model import (
from redis_om.model import Field, HashModel
from redis_om.model.migrations.migrator import Migrator
from redis_om.model.model import (
NotFoundError,
QueryNotSupportedError,
RedisModelError,

View file

@ -1,4 +1,5 @@
import abc
import asyncio
import datetime
import decimal
from collections import namedtuple
@ -8,9 +9,9 @@ from unittest import mock
import pytest
from pydantic import ValidationError
from redis_developer.model import EmbeddedJsonModel, Field, JsonModel
from redis_developer.model.migrations.migrator import Migrator
from redis_developer.model.model import (
from redis_om.model import EmbeddedJsonModel, Field, JsonModel
from redis_om.model.migrations.migrator import Migrator
from redis_om.model.model import (
NotFoundError,
QueryNotSupportedError,
RedisModelError,
@ -21,7 +22,7 @@ today = datetime.date.today()
@pytest.fixture
def m(key_prefix):
async def m(key_prefix, redis):
class BaseJsonModel(JsonModel, abc.ABC):
class Meta:
global_key_prefix = key_prefix
@ -64,7 +65,7 @@ def m(key_prefix):
# Creates an embedded list of models.
orders: Optional[List[Order]]
Migrator().run()
await Migrator(redis).run()
return namedtuple(
"Models", ["BaseJsonModel", "Note", "Address", "Item", "Order", "Member"]
@ -83,7 +84,7 @@ def address(m):
@pytest.fixture()
def members(address, m):
async def members(address, m):
member1 = m.Member(
first_name="Andrew",
last_name="Brookins",
@ -111,14 +112,15 @@ def members(address, m):
address=address,
)
member1.save()
member2.save()
member3.save()
await member1.save()
await member2.save()
await member3.save()
yield member1, member2, member3
def test_validates_required_fields(address, m):
@pytest.mark.asyncio
async def test_validates_required_fields(address, m):
# Raises ValidationError address is required
with pytest.raises(ValidationError):
m.Member(
@ -129,7 +131,8 @@ def test_validates_required_fields(address, m):
)
def test_validates_field(address, m):
@pytest.mark.asyncio
async def test_validates_field(address, m):
# Raises ValidationError: join_date is not a date
with pytest.raises(ValidationError):
m.Member(
@ -141,7 +144,8 @@ def test_validates_field(address, m):
# Passes validation
def test_validation_passes(address, m):
@pytest.mark.asyncio
async def test_validation_passes(address, m):
member = m.Member(
first_name="Andrew",
last_name="Brookins",
@ -153,7 +157,10 @@ def test_validation_passes(address, m):
assert member.first_name == "Andrew"
def test_saves_model_and_creates_pk(address, m):
@pytest.mark.asyncio
async def test_saves_model_and_creates_pk(address, m, redis):
await Migrator(redis).run()
member = m.Member(
first_name="Andrew",
last_name="Brookins",
@ -163,15 +170,16 @@ def test_saves_model_and_creates_pk(address, m):
address=address,
)
# Save a model instance to Redis
member.save()
await member.save()
member2 = m.Member.get(member.pk)
member2 = await m.Member.get(member.pk)
assert member2 == member
assert member2.address == address
@pytest.mark.skip("Not implemented yet")
def test_saves_many(address, m):
@pytest.mark.asyncio
async def test_saves_many(address, m):
members = [
m.Member(
first_name="Andrew",
@ -193,9 +201,16 @@ def test_saves_many(address, m):
m.Member.add(members)
async def save(members):
for m in members:
await m.save()
return members
@pytest.mark.skip("Not ready yet")
def test_updates_a_model(members, m):
member1, member2, member3 = members
@pytest.mark.asyncio
async def test_updates_a_model(members, m):
member1, member2, member3 = await save(members)
# Or, with an implicit save:
member1.update(last_name="Smith")
@ -213,18 +228,20 @@ def test_updates_a_model(members, m):
)
def test_paginate_query(members, m):
@pytest.mark.asyncio
async def test_paginate_query(members, m):
member1, member2, member3 = members
actual = m.Member.find().sort_by("age").all(batch_size=1)
actual = await m.Member.find().sort_by("age").all(batch_size=1)
assert actual == [member2, member1, member3]
def test_access_result_by_index_cached(members, m):
@pytest.mark.asyncio
async def test_access_result_by_index_cached(members, m):
member1, member2, member3 = members
query = m.Member.find().sort_by("age")
# Load the cache, throw away the result.
assert query._model_cache == []
query.execute()
await query.execute()
assert query._model_cache == [member2, member1, member3]
# Access an item that should be in the cache.
@ -233,21 +250,23 @@ def test_access_result_by_index_cached(members, m):
assert not mock_db.called
def test_access_result_by_index_not_cached(members, m):
@pytest.mark.asyncio
async def test_access_result_by_index_not_cached(members, m):
member1, member2, member3 = members
query = m.Member.find().sort_by("age")
# Assert that we don't have any models in the cache yet -- we
# haven't made any requests of Redis.
assert query._model_cache == []
assert query[0] == member2
assert query[1] == member1
assert query[2] == member3
assert query.get_item(0) == member2
assert query.get_item(1) == member1
assert query.get_item(2) == member3
def test_in_query(members, m):
@pytest.mark.asyncio
async def test_in_query(members, m):
member1, member2, member3 = members
actual = (
actual = await (
m.Member.find(m.Member.pk << [member1.pk, member2.pk, member3.pk])
.sort_by("age")
.all()
@ -256,12 +275,13 @@ def test_in_query(members, m):
@pytest.mark.skip("Not implemented yet")
def test_update_query(members, m):
@pytest.mark.asyncio
async def test_update_query(members, m):
member1, member2, member3 = members
m.Member.find(m.Member.pk << [member1.pk, member2.pk, member3.pk]).update(
await m.Member.find(m.Member.pk << [member1.pk, member2.pk, member3.pk]).update(
first_name="Bobby"
)
actual = (
actual = await (
m.Member.find(m.Member.pk << [member1.pk, member2.pk, member3.pk])
.sort_by("age")
.all()
@ -270,24 +290,25 @@ def test_update_query(members, m):
assert all([m.name == "Bobby" for m in actual])
def test_exact_match_queries(members, m):
@pytest.mark.asyncio
async def test_exact_match_queries(members, m):
member1, member2, member3 = members
actual = m.Member.find(m.Member.last_name == "Brookins").sort_by("age").all()
actual = await m.Member.find(m.Member.last_name == "Brookins").sort_by("age").all()
assert actual == [member2, member1]
actual = m.Member.find(
actual = await m.Member.find(
(m.Member.last_name == "Brookins") & ~(m.Member.first_name == "Andrew")
).all()
assert actual == [member2]
actual = m.Member.find(~(m.Member.last_name == "Brookins")).all()
actual = await m.Member.find(~(m.Member.last_name == "Brookins")).all()
assert actual == [member3]
actual = m.Member.find(m.Member.last_name != "Brookins").all()
actual = await m.Member.find(m.Member.last_name != "Brookins").all()
assert actual == [member3]
actual = (
actual = await (
m.Member.find(
(m.Member.last_name == "Brookins") & (m.Member.first_name == "Andrew")
| (m.Member.first_name == "Kim")
@ -297,19 +318,20 @@ def test_exact_match_queries(members, m):
)
assert actual == [member2, member1]
actual = m.Member.find(
actual = await m.Member.find(
m.Member.first_name == "Kim", m.Member.last_name == "Brookins"
).all()
assert actual == [member2]
actual = m.Member.find(m.Member.address.city == "Portland").sort_by("age").all()
actual = await m.Member.find(m.Member.address.city == "Portland").sort_by("age").all()
assert actual == [member2, member1, member3]
def test_recursive_query_expression_resolution(members, m):
@pytest.mark.asyncio
async def test_recursive_query_expression_resolution(members, m):
member1, member2, member3 = members
actual = (
actual = await (
m.Member.find(
(m.Member.last_name == "Brookins")
| (m.Member.age == 100) & (m.Member.last_name == "Smith")
@ -320,13 +342,14 @@ def test_recursive_query_expression_resolution(members, m):
assert actual == [member2, member1, member3]
def test_recursive_query_field_resolution(members, m):
@pytest.mark.asyncio
async def test_recursive_query_field_resolution(members, m):
member1, _, _ = members
member1.address.note = m.Note(
description="Weird house", created_on=datetime.datetime.now()
)
member1.save()
actual = m.Member.find(m.Member.address.note.description == "Weird house").all()
await member1.save()
actual = await m.Member.find(m.Member.address.note.description == "Weird house").all()
assert actual == [member1]
member1.orders = [
@ -336,29 +359,31 @@ def test_recursive_query_field_resolution(members, m):
created_on=datetime.datetime.now(),
)
]
member1.save()
actual = m.Member.find(m.Member.orders.items.name == "Ball").all()
await member1.save()
actual = await m.Member.find(m.Member.orders.items.name == "Ball").all()
assert actual == [member1]
assert actual[0].orders[0].items[0].name == "Ball"
def test_full_text_search(members, m):
@pytest.mark.asyncio
async def test_full_text_search(members, m):
member1, member2, _ = members
member1.update(bio="Hates sunsets, likes beaches")
member2.update(bio="Hates beaches, likes forests")
await member1.update(bio="Hates sunsets, likes beaches")
await member2.update(bio="Hates beaches, likes forests")
actual = m.Member.find(m.Member.bio % "beaches").sort_by("age").all()
actual = await m.Member.find(m.Member.bio % "beaches").sort_by("age").all()
assert actual == [member2, member1]
actual = m.Member.find(m.Member.bio % "forests").all()
actual = await m.Member.find(m.Member.bio % "forests").all()
assert actual == [member2]
def test_tag_queries_boolean_logic(members, m):
@pytest.mark.asyncio
async def test_tag_queries_boolean_logic(members, m):
member1, member2, member3 = members
actual = (
m.Member.find(
await m.Member.find(
(m.Member.first_name == "Andrew") & (m.Member.last_name == "Brookins")
| (m.Member.last_name == "Smith")
)
@ -368,7 +393,8 @@ def test_tag_queries_boolean_logic(members, m):
assert actual == [member1, member3]
def test_tag_queries_punctuation(address, m):
@pytest.mark.asyncio
async def test_tag_queries_punctuation(address, m):
member1 = m.Member(
first_name="Andrew, the Michael",
last_name="St. Brookins-on-Pier",
@ -377,7 +403,7 @@ def test_tag_queries_punctuation(address, m):
join_date=today,
address=address,
)
member1.save()
await member1.save()
member2 = m.Member(
first_name="Bob",
@ -387,24 +413,25 @@ def test_tag_queries_punctuation(address, m):
join_date=today,
address=address,
)
member2.save()
await member2.save()
assert (
m.Member.find(m.Member.first_name == "Andrew, the Michael").first() == member1
await m.Member.find(m.Member.first_name == "Andrew, the Michael").first() == member1
)
assert (
m.Member.find(m.Member.last_name == "St. Brookins-on-Pier").first() == member1
await m.Member.find(m.Member.last_name == "St. Brookins-on-Pier").first() == member1
)
# Notice that when we index and query multiple values that use the internal
# TAG separator for single-value exact-match fields, like an indexed string,
# the queries will succeed. We apply a workaround that queries for the union
# of the two values separated by the tag separator.
assert m.Member.find(m.Member.email == "a|b@example.com").all() == [member1]
assert m.Member.find(m.Member.email == "a|villain@example.com").all() == [member2]
assert await m.Member.find(m.Member.email == "a|b@example.com").all() == [member1]
assert await m.Member.find(m.Member.email == "a|villain@example.com").all() == [member2]
def test_tag_queries_negation(members, m):
@pytest.mark.asyncio
async def test_tag_queries_negation(members, m):
member1, member2, member3 = members
"""
@ -414,7 +441,7 @@ def test_tag_queries_negation(members, m):
"""
query = m.Member.find(~(m.Member.first_name == "Andrew"))
assert query.all() == [member2]
assert await query.all() == [member2]
"""
first_name
@ -429,7 +456,7 @@ def test_tag_queries_negation(members, m):
query = m.Member.find(
~(m.Member.first_name == "Andrew") & (m.Member.last_name == "Brookins")
)
assert query.all() == [member2]
assert await query.all() == [member2]
"""
first_name
@ -448,7 +475,7 @@ def test_tag_queries_negation(members, m):
~(m.Member.first_name == "Andrew")
& ((m.Member.last_name == "Brookins") | (m.Member.last_name == "Smith"))
)
assert query.all() == [member2]
assert await query.all() == [member2]
"""
first_name
@ -467,67 +494,71 @@ def test_tag_queries_negation(members, m):
~(m.Member.first_name == "Andrew") & (m.Member.last_name == "Brookins")
| (m.Member.last_name == "Smith")
)
assert query.sort_by("age").all() == [member2, member3]
assert await query.sort_by("age").all() == [member2, member3]
actual = m.Member.find(
actual = await m.Member.find(
(m.Member.first_name == "Andrew") & ~(m.Member.last_name == "Brookins")
).all()
assert actual == [member3]
def test_numeric_queries(members, m):
@pytest.mark.asyncio
async def test_numeric_queries(members, m):
member1, member2, member3 = members
actual = m.Member.find(m.Member.age == 34).all()
actual = await m.Member.find(m.Member.age == 34).all()
assert actual == [member2]
actual = m.Member.find(m.Member.age > 34).all()
actual = await m.Member.find(m.Member.age > 34).all()
assert actual == [member1, member3]
actual = m.Member.find(m.Member.age < 35).all()
actual = await m.Member.find(m.Member.age < 35).all()
assert actual == [member2]
actual = m.Member.find(m.Member.age <= 34).all()
actual = await m.Member.find(m.Member.age <= 34).all()
assert actual == [member2]
actual = m.Member.find(m.Member.age >= 100).all()
actual = await m.Member.find(m.Member.age >= 100).all()
assert actual == [member3]
actual = m.Member.find(~(m.Member.age == 100)).sort_by("age").all()
actual = await m.Member.find(~(m.Member.age == 100)).sort_by("age").all()
assert actual == [member2, member1]
actual = m.Member.find(m.Member.age > 30, m.Member.age < 40).sort_by("age").all()
actual = await m.Member.find(m.Member.age > 30, m.Member.age < 40).sort_by("age").all()
assert actual == [member2, member1]
actual = m.Member.find(m.Member.age != 34).sort_by("age").all()
actual = await m.Member.find(m.Member.age != 34).sort_by("age").all()
assert actual == [member1, member3]
def test_sorting(members, m):
@pytest.mark.asyncio
async def test_sorting(members, m):
member1, member2, member3 = members
actual = m.Member.find(m.Member.age > 34).sort_by("age").all()
actual = await m.Member.find(m.Member.age > 34).sort_by("age").all()
assert actual == [member1, member3]
actual = m.Member.find(m.Member.age > 34).sort_by("-age").all()
actual = await m.Member.find(m.Member.age > 34).sort_by("-age").all()
assert actual == [member3, member1]
with pytest.raises(QueryNotSupportedError):
# This field does not exist.
m.Member.find().sort_by("not-a-real-field").all()
await m.Member.find().sort_by("not-a-real-field").all()
with pytest.raises(QueryNotSupportedError):
# This field is not sortable.
m.Member.find().sort_by("join_date").all()
await m.Member.find().sort_by("join_date").all()
def test_not_found(m):
@pytest.mark.asyncio
async def test_not_found(m):
with pytest.raises(NotFoundError):
# This ID does not exist.
m.Member.get(1000)
await m.Member.get(1000)
def test_list_field_limitations(m):
@pytest.mark.asyncio
async def test_list_field_limitations(m, redis):
with pytest.raises(RedisModelError):
class SortableTarotWitch(m.BaseJsonModel):
@ -571,15 +602,16 @@ def test_list_field_limitations(m):
# We need to import and run this manually because we defined
# our model classes within a function that runs after the test
# suite's migrator has already looked for migrations to run.
Migrator().run()
await Migrator(redis).run()
witch = TarotWitch(tarot_cards=["death"])
witch.save()
actual = TarotWitch.find(TarotWitch.tarot_cards << "death").all()
await witch.save()
actual = await TarotWitch.find(TarotWitch.tarot_cards << "death").all()
assert actual == [witch]
def test_schema(m, key_prefix):
@pytest.mark.asyncio
async def test_schema(m, key_prefix):
assert (
m.Member.redisearch_schema()
== f"ON JSON PREFIX 1 {key_prefix}:tests.test_json_model.Member: SCHEMA $.pk AS pk TAG SEPARATOR | $.first_name AS first_name TAG SEPARATOR | $.last_name AS last_name TAG SEPARATOR | $.email AS email TAG SEPARATOR | $.age AS age NUMERIC $.bio AS bio TAG SEPARATOR | $.bio AS bio_fts TEXT $.address.pk AS address_pk TAG SEPARATOR | $.address.city AS address_city TAG SEPARATOR | $.address.postal_code AS address_postal_code TAG SEPARATOR | $.address.note.pk AS address_note_pk TAG SEPARATOR | $.address.note.description AS address_note_description TAG SEPARATOR | $.orders[*].pk AS orders_pk TAG SEPARATOR | $.orders[*].items[*].pk AS orders_items_pk TAG SEPARATOR | $.orders[*].items[*].name AS orders_items_name TAG SEPARATOR |"