Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
vladvildanov authored Jan 28, 2025
2 parents 2a1ff87 + 03031a2 commit cb07272
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 64 deletions.
10 changes: 7 additions & 3 deletions .github/actions/run-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ runs:
CLIENT_LIBS_TEST_IMAGE: "redislabs/client-libs-test:${{ inputs.redis-version }}"
run: |
set -e
if [ "${{inputs.redis-version}}" == "8.0-M04-pre" ]; then
export REDIS_IMAGE=redis:8.0-M03
fi
echo "::group::Installing dependencies"
pip install -U setuptools wheel
Expand All @@ -56,9 +60,9 @@ runs:
# Mapping of redis version to stack version
declare -A redis_stack_version_mapping=(
["7.4.1"]="7.4.0-v1"
["7.2.6"]="7.2.0-v13"
["6.2.16"]="6.2.6-v17"
["7.4.2"]="7.4.0-v2"
["7.2.7"]="7.2.0-v14"
["6.2.17"]="6.2.6-v18"
)
if [[ -v redis_stack_version_mapping[$REDIS_VERSION] ]]; then
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ env:
COVERAGE_CORE: sysmon
REDIS_IMAGE: redis:latest
REDIS_STACK_IMAGE: redis/redis-stack-server:latest
CURRENT_REDIS_VERSION: '7.4.1'
CURRENT_REDIS_VERSION: '7.4.2'

jobs:
dependency-audit:
Expand Down Expand Up @@ -74,7 +74,7 @@ jobs:
max-parallel: 15
fail-fast: false
matrix:
redis-version: ['8.0-M02', '${{ needs.redis_version.outputs.CURRENT }}', '7.2.6', '6.2.16']
redis-version: ['8.0-M04-pre', '${{ needs.redis_version.outputs.CURRENT }}', '7.2.7', '6.2.17']
python-version: ['3.8', '3.12']
parser-backend: ['plain']
event-loop: ['asyncio']
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ services:
- all

redis-stack:
image: ${REDIS_STACK_IMAGE:-redis/redis-stack-server:edge}
image: ${REDIS_STACK_IMAGE:-redis/redis-stack-server:latest}
container_name: redis-stack
ports:
- 6479:6379
Expand All @@ -112,6 +112,7 @@ services:
profiles:
- standalone
- all-stack
- all

redis-stack-graph:
image: redis/redis-stack-server:6.2.6-v15
Expand Down
2 changes: 2 additions & 0 deletions redis/commands/search/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ def scorer(self, scorer: str) -> "Query":
Use a different scoring function to evaluate document relevance.
Default is `TFIDF`.
Since Redis 8.0 default was changed to BM25STD.
:param scorer: The scoring function to use
(e.g. `TFIDF.DOCNORM` or `BM25`)
"""
Expand Down
30 changes: 14 additions & 16 deletions tests/test_asyncio/test_connection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import socket
import types
from errno import ECONNREFUSED
from unittest.mock import patch

import pytest
Expand Down Expand Up @@ -36,15 +37,16 @@ async def test_invalid_response(create_redis):
fake_stream = MockStream(raw + b"\r\n")

parser: _AsyncRESPBase = r.connection._parser
with mock.patch.object(parser, "_stream", fake_stream):
with pytest.raises(InvalidResponse) as cm:
await parser.read_response()

if isinstance(parser, _AsyncRESPBase):
assert str(cm.value) == f"Protocol Error: {raw!r}"
exp_err = f"Protocol Error: {raw!r}"
else:
assert (
str(cm.value) == f'Protocol error, got "{raw.decode()}" as reply type byte'
)
exp_err = f'Protocol error, got "{raw.decode()}" as reply type byte'

with mock.patch.object(parser, "_stream", fake_stream):
with pytest.raises(InvalidResponse, match=exp_err):
await parser.read_response()

await r.connection.disconnect()


Expand Down Expand Up @@ -170,10 +172,9 @@ async def test_connect_timeout_error_without_retry():
conn._connect = mock.AsyncMock()
conn._connect.side_effect = socket.timeout

with pytest.raises(TimeoutError) as e:
with pytest.raises(TimeoutError, match="Timeout connecting to server"):
await conn.connect()
assert conn._connect.call_count == 1
assert str(e.value) == "Timeout connecting to server"


@pytest.mark.onlynoncluster
Expand Down Expand Up @@ -531,17 +532,14 @@ async def test_format_error_message(conn, error, expected_message):


async def test_network_connection_failure():
with pytest.raises(ConnectionError) as e:
exp_err = rf"^Error {ECONNREFUSED} connecting to 127.0.0.1:9999.(.+)$"
with pytest.raises(ConnectionError, match=exp_err):
redis = Redis(host="127.0.0.1", port=9999)
await redis.set("a", "b")
assert str(e.value).startswith("Error 111 connecting to 127.0.0.1:9999. Connect")


async def test_unix_socket_connection_failure():
with pytest.raises(ConnectionError) as e:
exp_err = "Error 2 connecting to unix:///tmp/a.sock. No such file or directory."
with pytest.raises(ConnectionError, match=exp_err):
redis = Redis(unix_socket_path="unix:///tmp/a.sock")
await redis.set("a", "b")
assert (
str(e.value)
== "Error 2 connecting to unix:///tmp/a.sock. No such file or directory."
)
94 changes: 91 additions & 3 deletions tests/test_asyncio/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ async def test_client(decoded_r: redis.Redis):

@pytest.mark.redismod
@pytest.mark.onlynoncluster
@skip_if_server_version_gte("7.9.0")
async def test_scores(decoded_r: redis.Redis):
await decoded_r.ft().create_index((TextField("txt"),))

Expand All @@ -361,6 +362,29 @@ async def test_scores(decoded_r: redis.Redis):
assert "doc1" == res["results"][1]["id"]


@pytest.mark.redismod
@pytest.mark.onlynoncluster
@skip_if_server_version_lt("7.9.0")
async def test_scores_with_new_default_scorer(decoded_r: redis.Redis):
await decoded_r.ft().create_index((TextField("txt"),))

await decoded_r.hset("doc1", mapping={"txt": "foo baz"})
await decoded_r.hset("doc2", mapping={"txt": "foo bar"})

q = Query("foo ~bar").with_scores()
res = await decoded_r.ft().search(q)
if is_resp2_connection(decoded_r):
assert 2 == res.total
assert "doc2" == res.docs[0].id
assert 0.87 == pytest.approx(res.docs[0].score, 0.01)
assert "doc1" == res.docs[1].id
else:
assert 2 == res["total_results"]
assert "doc2" == res["results"][0]["id"]
assert 0.87 == pytest.approx(res["results"][0]["score"], 0.01)
assert "doc1" == res["results"][1]["id"]


@pytest.mark.redismod
async def test_stopwords(decoded_r: redis.Redis):
stopwords = ["foo", "bar", "baz"]
Expand Down Expand Up @@ -663,7 +687,7 @@ async def test_summarize(decoded_r: redis.Redis):
await createIndex(decoded_r.ft())
await waitForIndex(decoded_r, "idx")

q = Query("king henry").paging(0, 1)
q = Query('"king henry"').paging(0, 1)
q.highlight(fields=("play", "txt"), tags=("<b>", "</b>"))
q.summarize("txt")

Expand All @@ -675,7 +699,7 @@ async def test_summarize(decoded_r: redis.Redis):
== doc.txt
)

q = Query("king henry").paging(0, 1).summarize().highlight()
q = Query('"king henry"').paging(0, 1).summarize().highlight()

doc = sorted((await decoded_r.ft().search(q)).docs)[0]
assert "<b>Henry</b> ... " == doc.play
Expand All @@ -691,7 +715,7 @@ async def test_summarize(decoded_r: redis.Redis):
== doc["extra_attributes"]["txt"]
)

q = Query("king henry").paging(0, 1).summarize().highlight()
q = Query('"king henry"').paging(0, 1).summarize().highlight()

doc = sorted((await decoded_r.ft().search(q))["results"])[0]
assert "<b>Henry</b> ... " == doc["extra_attributes"]["play"]
Expand Down Expand Up @@ -1029,6 +1053,7 @@ async def test_phonetic_matcher(decoded_r: redis.Redis):
@pytest.mark.onlynoncluster
# NOTE(imalinovskyi): This test contains hardcoded scores valid only for RediSearch 2.8+
@skip_ifmodversion_lt("2.8.0", "search")
@skip_if_server_version_gte("7.9.0")
async def test_scorer(decoded_r: redis.Redis):
await decoded_r.ft().create_index((TextField("description"),))

Expand Down Expand Up @@ -1087,6 +1112,69 @@ async def test_scorer(decoded_r: redis.Redis):
assert 0.0 == res["results"][0]["score"]


@pytest.mark.redismod
@pytest.mark.onlynoncluster
# NOTE(imalinovskyi): This test contains hardcoded scores valid only for RediSearch 2.8+
@skip_ifmodversion_lt("2.8.0", "search")
@skip_if_server_version_lt("7.9.0")
async def test_scorer_with_new_default_scorer(decoded_r: redis.Redis):
await decoded_r.ft().create_index((TextField("description"),))

await decoded_r.hset(
"doc1", mapping={"description": "The quick brown fox jumps over the lazy dog"}
)
await decoded_r.hset(
"doc2",
mapping={
"description": "Quick alice was beginning to get very tired of sitting by her quick sister on the bank, and of having nothing to do." # noqa
},
)

if is_resp2_connection(decoded_r):
# default scorer is BM25STD
res = await decoded_r.ft().search(Query("quick").with_scores())
assert 0.23 == pytest.approx(res.docs[0].score, 0.05)
res = await decoded_r.ft().search(Query("quick").scorer("TFIDF").with_scores())
assert 1.0 == res.docs[0].score
res = await decoded_r.ft().search(
Query("quick").scorer("TFIDF.DOCNORM").with_scores()
)
assert 0.14285714285714285 == res.docs[0].score
res = await decoded_r.ft().search(Query("quick").scorer("BM25").with_scores())
assert 0.22471909420069797 == res.docs[0].score
res = await decoded_r.ft().search(Query("quick").scorer("DISMAX").with_scores())
assert 2.0 == res.docs[0].score
res = await decoded_r.ft().search(
Query("quick").scorer("DOCSCORE").with_scores()
)
assert 1.0 == res.docs[0].score
res = await decoded_r.ft().search(
Query("quick").scorer("HAMMING").with_scores()
)
assert 0.0 == res.docs[0].score
else:
res = await decoded_r.ft().search(Query("quick").with_scores())
assert 0.23 == pytest.approx(res["results"][0]["score"], 0.05)
res = await decoded_r.ft().search(Query("quick").scorer("TFIDF").with_scores())
assert 1.0 == res["results"][0]["score"]
res = await decoded_r.ft().search(
Query("quick").scorer("TFIDF.DOCNORM").with_scores()
)
assert 0.14285714285714285 == res["results"][0]["score"]
res = await decoded_r.ft().search(Query("quick").scorer("BM25").with_scores())
assert 0.22471909420069797 == res["results"][0]["score"]
res = await decoded_r.ft().search(Query("quick").scorer("DISMAX").with_scores())
assert 2.0 == res["results"][0]["score"]
res = await decoded_r.ft().search(
Query("quick").scorer("DOCSCORE").with_scores()
)
assert 1.0 == res["results"][0]["score"]
res = await decoded_r.ft().search(
Query("quick").scorer("HAMMING").with_scores()
)
assert 0.0 == res["results"][0]["score"]


@pytest.mark.redismod
async def test_get(decoded_r: redis.Redis):
await decoded_r.ft().create_index((TextField("f1"), TextField("f2")))
Expand Down
23 changes: 0 additions & 23 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4332,7 +4332,6 @@ def test_xgroup_create_mkstream(self, r):
assert r.xinfo_groups(stream) == expected

@skip_if_server_version_lt("7.0.0")
@skip_if_server_version_gte("7.9.0")
def test_xgroup_create_entriesread(self, r: redis.Redis):
stream = "stream"
group = "group"
Expand All @@ -4341,28 +4340,6 @@ def test_xgroup_create_entriesread(self, r: redis.Redis):
# no group is setup yet, no info to obtain
assert r.xinfo_groups(stream) == []

assert r.xgroup_create(stream, group, 0, entries_read=7)
expected = [
{
"name": group.encode(),
"consumers": 0,
"pending": 0,
"last-delivered-id": b"0-0",
"entries-read": 7,
"lag": -6,
}
]
assert r.xinfo_groups(stream) == expected

@skip_if_server_version_lt("7.9.0")
def test_xgroup_create_entriesread_with_fixed_lag_field(self, r: redis.Redis):
stream = "stream"
group = "group"
r.xadd(stream, {"foo": "bar"})

# no group is setup yet, no info to obtain
assert r.xinfo_groups(stream) == []

assert r.xgroup_create(stream, group, 0, entries_read=7)
expected = [
{
Expand Down
18 changes: 7 additions & 11 deletions tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sys
import threading
import types
from errno import ECONNREFUSED
from typing import Any
from unittest import mock
from unittest.mock import call, patch
Expand Down Expand Up @@ -44,9 +45,8 @@ def test_invalid_response(r):
raw = b"x"
parser = r.connection._parser
with mock.patch.object(parser._buffer, "readline", return_value=raw):
with pytest.raises(InvalidResponse) as cm:
with pytest.raises(InvalidResponse, match=f"Protocol Error: {raw!r}"):
parser.read_response()
assert str(cm.value) == f"Protocol Error: {raw!r}"


@skip_if_server_version_lt("4.0.0")
Expand Down Expand Up @@ -141,10 +141,9 @@ def test_connect_timeout_error_without_retry(self):
conn._connect = mock.Mock()
conn._connect.side_effect = socket.timeout

with pytest.raises(TimeoutError) as e:
with pytest.raises(TimeoutError, match="Timeout connecting to server"):
conn.connect()
assert conn._connect.call_count == 1
assert str(e.value) == "Timeout connecting to server"
self.clear(conn)


Expand Down Expand Up @@ -349,20 +348,17 @@ def test_format_error_message(conn, error, expected_message):


def test_network_connection_failure():
with pytest.raises(ConnectionError) as e:
exp_err = f"Error {ECONNREFUSED} connecting to localhost:9999. Connection refused."
with pytest.raises(ConnectionError, match=exp_err):
redis = Redis(port=9999)
redis.set("a", "b")
assert str(e.value) == "Error 111 connecting to localhost:9999. Connection refused."


def test_unix_socket_connection_failure():
with pytest.raises(ConnectionError) as e:
exp_err = "Error 2 connecting to unix:///tmp/a.sock. No such file or directory."
with pytest.raises(ConnectionError, match=exp_err):
redis = Redis(unix_socket_path="unix:///tmp/a.sock")
redis.set("a", "b")
assert (
str(e.value)
== "Error 2 connecting to unix:///tmp/a.sock. No such file or directory."
)


class TestUnitConnectionPool:
Expand Down
4 changes: 4 additions & 0 deletions tests/test_multiprocessing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import contextlib
import multiprocessing
import sys

import pytest
import redis
Expand All @@ -8,6 +9,9 @@

from .conftest import _get_client

if sys.platform == "darwin":
multiprocessing.set_start_method("fork", force=True)


@contextlib.contextmanager
def exit_callback(callback, *args):
Expand Down
Loading

0 comments on commit cb07272

Please sign in to comment.