Files
wehub-resource-sync dc6079821b
Docs Tests / Check for file changes (push) Has been cancelled
Docs Tests / Test Documentation (push) Has been cancelled
Docs Tests / Documentation Linting Checks (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.9) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.9) (push) Has been cancelled
Continuous Integration / Check for file changes (push) Has been cancelled
Continuous Integration / Wait for docs tests (push) Has been cancelled
Continuous Integration / Code Quality (push) Has been cancelled
Continuous Integration / Check for changelog (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Upload coverage reports to codeclimate (push) Has been cancelled
Continuous Integration / Run Non-Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Run Broker Integration Tests (push) Has been cancelled
Continuous Integration / Run Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Build Docker base images and setup environment (push) Has been cancelled
Continuous Integration / Build Docker (default) (push) Has been cancelled
Continuous Integration / Build Docker (full) (push) Has been cancelled
Continuous Integration / Build Docker (mitie-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-de) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-it) (push) Has been cancelled
Continuous Integration / Deploy to PyPI (push) Has been cancelled
Continuous Integration / Notify Slack & Publish Release Notes (push) Has been cancelled
Publish Documentation / Evaluate release tag (push) Has been cancelled
Publish Documentation / Prebuild Docs (push) Has been cancelled
Publish Documentation / Preview Docs (push) Has been cancelled
Publish Documentation / Check for file changes (push) Has been cancelled
Publish Documentation / Publish Docs (push) Has been cancelled
Automatic PR Merger / mergepal (push) Has been cancelled
CI Github Actions / Run Tests (push) Has been cancelled
Semgrep / Semgrep Workflow Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:47 +08:00

404 lines
13 KiB
Python

import asyncio
import logging
import sys
import time
from pathlib import Path
from typing import Text
from unittest.mock import Mock, patch
import numpy as np
import pytest
import rasa.core.lock_store
from _pytest.logging import LogCaptureFixture
from _pytest.monkeypatch import MonkeyPatch
from rasa.core.agent import Agent
from rasa.core.channels import UserMessage
from rasa.core.constants import DEFAULT_LOCK_LIFETIME
from rasa.core.lock import TicketLock
from rasa.core.lock_store import (
DEFAULT_REDIS_LOCK_STORE_KEY_PREFIX,
InMemoryLockStore,
LockError,
LockStore,
RedisLockStore,
)
from rasa.shared.constants import INTENT_MESSAGE_PREFIX
from rasa.shared.exceptions import ConnectionException
from rasa.utils.endpoints import EndpointConfig, read_endpoint_config
class FakeRedisLockStore(RedisLockStore):
"""Fake `RedisLockStore` using `fakeredis` library."""
# skipcq: PYL-W0231
# noinspection PyMissingConstructor
def __init__(self):
import fakeredis
self.red = fakeredis.FakeStrictRedis()
# added in redis==3.3.0, but not yet in fakeredis
self.red.connection_pool.connection_class.health_check_interval = 0
self.key_prefix = DEFAULT_REDIS_LOCK_STORE_KEY_PREFIX
def test_issue_ticket():
lock = TicketLock("random id 0")
# no lock issued
assert lock.last_issued == -1
assert lock.now_serving == 0
# no one is waiting
assert not lock.is_someone_waiting()
# issue ticket
ticket = lock.issue_ticket(1)
assert ticket == 0
assert lock.last_issued == 0
assert lock.now_serving == 0
# someone is waiting
assert lock.is_someone_waiting()
def test_remove_expired_tickets():
lock = TicketLock("random id 1")
# issue one long- and one short-lived ticket
_ = list(map(lock.issue_ticket, [k for k in [0.01, 10]]))
# both tickets are there
assert len(lock.tickets) == 2
# sleep and only one ticket should be left
time.sleep(0.02)
lock.remove_expired_tickets()
assert len(lock.tickets) == 1
@pytest.mark.parametrize("lock_store", [InMemoryLockStore(), FakeRedisLockStore()])
def test_create_lock_store(lock_store: LockStore):
conversation_id = "my id 0"
# create and lock
lock = lock_store.create_lock(conversation_id)
lock_store.save_lock(lock)
lock = lock_store.get_lock(conversation_id)
assert lock
assert lock.conversation_id == conversation_id
def test_raise_connection_exception_redis_lock_store(monkeypatch: MonkeyPatch):
monkeypatch.setattr(
rasa.core.lock_store, "RedisLockStore", Mock(side_effect=ConnectionError())
)
with pytest.raises(ConnectionException):
LockStore.create(
EndpointConfig(username="username", password="password", type="redis")
)
@pytest.mark.parametrize("lock_store", [InMemoryLockStore(), FakeRedisLockStore()])
def test_serve_ticket(lock_store: LockStore):
conversation_id = "my id 1"
lock = lock_store.create_lock(conversation_id)
lock_store.save_lock(lock)
# issue ticket with long lifetime
ticket_0 = lock_store.issue_ticket(conversation_id, 10)
assert ticket_0 == 0
lock = lock_store.get_lock(conversation_id)
assert lock.last_issued == ticket_0
assert lock.now_serving == ticket_0
assert lock.is_someone_waiting()
# issue another ticket
ticket_1 = lock_store.issue_ticket(conversation_id, 10)
# finish serving ticket_0
lock_store.finish_serving(conversation_id, ticket_0)
lock = lock_store.get_lock(conversation_id)
assert lock.last_issued == ticket_1
assert lock.now_serving == ticket_1
assert lock.is_someone_waiting()
# serve second ticket and no one should be waiting
lock_store.finish_serving(conversation_id, ticket_1)
lock = lock_store.get_lock(conversation_id)
assert not lock.is_someone_waiting()
# noinspection PyProtectedMember
@pytest.mark.parametrize("lock_store", [InMemoryLockStore(), FakeRedisLockStore()])
def test_lock_expiration(lock_store: LockStore):
conversation_id = "my id 2"
lock = lock_store.create_lock(conversation_id)
lock_store.save_lock(lock)
# issue ticket with long lifetime
ticket = lock.issue_ticket(10)
assert ticket == 0
assert not lock._ticket_for_ticket_number(ticket).has_expired()
# issue ticket with short lifetime
ticket = lock.issue_ticket(0.00001)
time.sleep(0.00002)
assert ticket == 1
assert lock._ticket_for_ticket_number(ticket) is None
# newly assigned ticket should get number 1 again
assert lock.issue_ticket(10) == 1
async def test_multiple_conversation_ids(default_agent: Agent):
text = INTENT_MESSAGE_PREFIX + 'greet{"name":"Rasa"}'
conversation_ids = [f"conversation {i}" for i in range(2)]
# ensure conversations are processed in order
tasks = [default_agent.handle_text(text, sender_id=_id) for _id in conversation_ids]
results = await asyncio.gather(*tasks)
assert results
processed_ids = [result[0]["recipient_id"] for result in results]
assert processed_ids == conversation_ids
@pytest.mark.xfail(
sys.platform == "win32",
reason="This test sometimes fails on Windows. We want to investigate it further",
)
async def test_message_order(tmp_path: Path, default_agent: Agent):
start_time = time.time()
n_messages = 10
lock_wait = 0.5
# let's write the incoming order of messages and the order of results to temp files
results_file = tmp_path / "results_file"
incoming_order_file = tmp_path / "incoming_order_file"
# We need to mock `Agent.handle_message()` so we can introduce an
# artificial holdup (`wait_time_in_seconds`). In the mocked method, we'll
# record messages as they come and and as they're processed in files so we
# can check the order later on. We don't need the return value of this method so
# we'll just return None.
async def mocked_handle_message(self, message: UserMessage, wait: float) -> None:
# write incoming message to file
with open(str(incoming_order_file), "a+") as f_0:
f_0.write(message.text + "\n")
async with self.lock_store.lock(
message.sender_id, wait_time_in_seconds=lock_wait
):
# hold up the message processing after the lock has been acquired
await asyncio.sleep(wait)
# write message to file as it's processed
with open(str(results_file), "a+") as f_1:
f_1.write(message.text + "\n")
return None
# We'll send n_messages from the same sender_id with different blocking times
# after the lock has been acquired.
# We have to ensure that the messages are processed in the right order.
with patch.object(Agent, "handle_message", mocked_handle_message):
# use decreasing wait times so that every message after the first one
# does not acquire its lock immediately
wait_times = np.linspace(0.1, 0.05, n_messages)
tasks = [
default_agent.handle_message(
UserMessage(f"sender {i}", sender_id="some id"), wait=k
)
for i, k in enumerate(wait_times)
]
# execute futures
await asyncio.gather(*(asyncio.ensure_future(t) for t in tasks))
expected_order = [f"sender {i}" for i in range(len(wait_times))]
# ensure order of incoming messages is as expected
with open(str(incoming_order_file)) as f:
incoming_order = [line for line in f.read().split("\n") if line]
assert incoming_order == expected_order
# ensure results are processed in expected order
with open(str(results_file)) as f:
results_order = [line for line in f.read().split("\n") if line]
assert results_order == expected_order
# Every message after the first one will wait `lock_wait` seconds to acquire its
# lock (`wait_time_in_seconds` kwarg in `lock_store.lock()`).
# Let's make sure that this is not blocking and test that total test
# execution time is less than the sum of all wait times plus
# (n_messages - 1) * lock_wait
time_limit = np.sum(wait_times[1:])
time_limit += (n_messages - 1) * lock_wait
assert time.time() - start_time < time_limit
@pytest.mark.xfail(
sys.platform == "win32",
reason="This test sometimes fails on Windows. We want to investigate it further",
)
async def test_lock_error(default_agent: Agent):
lock_lifetime = 0.01
wait_time_in_seconds = 0.01
holdup = 0.5
# Mock message handler again to add a wait time holding up the lock
# after it's been acquired
async def mocked_handle_message(self, message: UserMessage) -> None:
async with self.lock_store.lock(
message.sender_id,
wait_time_in_seconds=wait_time_in_seconds,
lock_lifetime=lock_lifetime,
):
# hold up the message processing after the lock has been acquired
await asyncio.sleep(holdup)
return None
with patch.object(Agent, "handle_message", mocked_handle_message):
# first message blocks the lock for `holdup`,
# meaning the second message will not be able to acquire a lock
tasks = [
default_agent.handle_message(
UserMessage(f"sender {i}", sender_id="some id")
)
for i in range(2)
]
with pytest.raises(LockError):
await asyncio.gather(*(asyncio.ensure_future(t) for t in tasks))
async def test_lock_lifetime_environment_variable(monkeypatch: MonkeyPatch):
import rasa.core.lock_store
# by default lock lifetime is `DEFAULT_LOCK_LIFETIME`
assert rasa.core.lock_store._get_lock_lifetime() == DEFAULT_LOCK_LIFETIME
# set new lock lifetime as environment variable
new_lock_lifetime = 123
monkeypatch.setenv("TICKET_LOCK_LIFETIME", str(new_lock_lifetime))
assert rasa.core.lock_store._get_lock_lifetime() == new_lock_lifetime
@pytest.mark.parametrize("lock_store", [InMemoryLockStore(), FakeRedisLockStore()])
async def test_acquire_lock_debug_message(
lock_store: LockStore, caplog: LogCaptureFixture
):
conversation_id = "test_acquire_lock_debug_message"
wait_time_in_seconds = 0.01
async def locking_task() -> None:
async with lock_store.lock(
conversation_id, wait_time_in_seconds=wait_time_in_seconds
):
# Do a very short sleep so that the other tasks can try to acquire the lock
# in the meantime
await asyncio.sleep(0.0)
with caplog.at_level(logging.DEBUG):
await asyncio.gather(
locking_task(), # Gets served immediately
locking_task(), # Gets served second
locking_task(), # Gets served last
)
assert any(
f"because 1 other item(s) for this conversation ID have to be finished "
f"processing first. Retrying in {wait_time_in_seconds} seconds ..." in message
for message in caplog.messages
)
assert any(
f"because 2 other item(s) for this conversation ID have to be finished "
f"processing first. Retrying in {wait_time_in_seconds} seconds ..." in message
for message in caplog.messages
)
async def test_redis_lock_store_timeout(monkeypatch: MonkeyPatch):
import redis.exceptions
lock_store = FakeRedisLockStore()
monkeypatch.setattr(
lock_store,
lock_store.get_or_create_lock.__name__,
Mock(side_effect=redis.exceptions.TimeoutError),
)
with pytest.raises(LockError):
async with lock_store.lock("some sender"):
pass
async def test_redis_lock_store_with_invalid_prefix(monkeypatch: MonkeyPatch):
import redis.exceptions
lock_store = FakeRedisLockStore()
prefix = "!asdf234 34#"
lock_store._set_key_prefix(prefix)
assert lock_store.key_prefix == DEFAULT_REDIS_LOCK_STORE_KEY_PREFIX
monkeypatch.setattr(
lock_store,
lock_store.get_or_create_lock.__name__,
Mock(side_effect=redis.exceptions.TimeoutError),
)
with pytest.raises(LockError):
async with lock_store.lock("some sender"):
pass
async def test_redis_lock_store_with_valid_prefix(monkeypatch: MonkeyPatch):
import redis.exceptions
lock_store = FakeRedisLockStore()
prefix = "chatbot42"
lock_store._set_key_prefix(prefix)
assert lock_store.key_prefix == prefix + ":" + DEFAULT_REDIS_LOCK_STORE_KEY_PREFIX
monkeypatch.setattr(
lock_store,
lock_store.get_or_create_lock.__name__,
Mock(side_effect=redis.exceptions.TimeoutError),
)
with pytest.raises(LockError):
async with lock_store.lock("some sender"):
pass
def test_create_lock_store_from_endpoint_config(endpoints_path: Text):
store = read_endpoint_config(endpoints_path, endpoint_type="lock_store")
tracker_store = RedisLockStore(
host="localhost",
port=6379,
db=0,
username="username",
password="password",
use_ssl=True,
ssl_keyfile="keyfile.key",
ssl_certfile="certfile.crt",
ssl_ca_certs="my-bundle.ca-bundle",
key_prefix="lock",
)
assert isinstance(tracker_store, type(LockStore.create(store)))