standard-tests[minor]: Add standard tests for cache (#23357)

Add standard tests for cache abstraction
This commit is contained in:
Eugene Yurtsev 2024-06-24 11:15:03 -04:00 committed by GitHub
parent 987099cfcd
commit d90379210a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 253 additions and 3 deletions

View File

@ -0,0 +1,192 @@
from abc import ABC, abstractmethod
import pytest
from langchain_core.caches import BaseCache
from langchain_core.outputs import Generation
class SyncCacheTestSuite(ABC):
"""Test suite for checking the BaseCache API of a caching layer for LLMs.
This test suite verifies the basic caching API of a caching layer for LLMs.
The test suite is designed for synchronous caching layers.
Implementers should subclass this test suite and provide a fixture
that returns an empty cache for each test.
"""
@abstractmethod
@pytest.fixture
def cache(self) -> BaseCache:
"""Get the cache class to test.
The returned cache should be EMPTY.
"""
def get_sample_prompt(self) -> str:
"""Return a sample prompt for testing."""
return "Sample prompt for testing."
def get_sample_llm_string(self) -> str:
"""Return a sample LLM string for testing."""
return "Sample LLM string configuration."
def get_sample_generation(self) -> Generation:
"""Return a sample Generation object for testing."""
return Generation(
text="Sample generated text.", generation_info={"reason": "test"}
)
def test_cache_is_empty(self, cache: BaseCache) -> None:
"""Test that the cache is empty."""
assert (
cache.lookup(self.get_sample_prompt(), self.get_sample_llm_string()) is None
)
def test_update_cache(self, cache: BaseCache) -> None:
"""Test updating the cache."""
prompt = self.get_sample_prompt()
llm_string = self.get_sample_llm_string()
generation = self.get_sample_generation()
cache.update(prompt, llm_string, [generation])
assert cache.lookup(prompt, llm_string) == [generation]
def test_cache_still_empty(self, cache: BaseCache) -> None:
"""This test should follow a test that updates the cache.
This just verifies that the fixture is set up properly to be empty
after each test.
"""
assert (
cache.lookup(self.get_sample_prompt(), self.get_sample_llm_string()) is None
)
def test_clear_cache(self, cache: BaseCache) -> None:
"""Test clearing the cache."""
prompt = self.get_sample_prompt()
llm_string = self.get_sample_llm_string()
generation = self.get_sample_generation()
cache.update(prompt, llm_string, [generation])
cache.clear()
assert cache.lookup(prompt, llm_string) is None
def test_cache_miss(self, cache: BaseCache) -> None:
"""Test cache miss."""
assert cache.lookup("Nonexistent prompt", self.get_sample_llm_string()) is None
def test_cache_hit(self, cache: BaseCache) -> None:
"""Test cache hit."""
prompt = self.get_sample_prompt()
llm_string = self.get_sample_llm_string()
generation = self.get_sample_generation()
cache.update(prompt, llm_string, [generation])
assert cache.lookup(prompt, llm_string) == [generation]
def test_update_cache_with_multiple_generations(self, cache: BaseCache) -> None:
"""Test updating the cache with multiple Generation objects."""
prompt = self.get_sample_prompt()
llm_string = self.get_sample_llm_string()
generations = [
self.get_sample_generation(),
Generation(text="Another generated text."),
]
cache.update(prompt, llm_string, generations)
assert cache.lookup(prompt, llm_string) == generations
class AsyncCacheTestSuite(ABC):
"""Test suite for checking the BaseCache API of a caching layer for LLMs.
This test suite verifies the basic caching API of a caching layer for LLMs.
The test suite is designed for synchronous caching layers.
Implementers should subclass this test suite and provide a fixture
that returns an empty cache for each test.
"""
@abstractmethod
@pytest.fixture
async def cache(self) -> BaseCache:
"""Get the cache class to test.
The returned cache should be EMPTY.
"""
def get_sample_prompt(self) -> str:
"""Return a sample prompt for testing."""
return "Sample prompt for testing."
def get_sample_llm_string(self) -> str:
"""Return a sample LLM string for testing."""
return "Sample LLM string configuration."
def get_sample_generation(self) -> Generation:
"""Return a sample Generation object for testing."""
return Generation(
text="Sample generated text.", generation_info={"reason": "test"}
)
async def test_cache_is_empty(self, cache: BaseCache) -> None:
"""Test that the cache is empty."""
assert (
await cache.alookup(self.get_sample_prompt(), self.get_sample_llm_string())
is None
)
async def test_update_cache(self, cache: BaseCache) -> None:
"""Test updating the cache."""
prompt = self.get_sample_prompt()
llm_string = self.get_sample_llm_string()
generation = self.get_sample_generation()
await cache.aupdate(prompt, llm_string, [generation])
assert await cache.alookup(prompt, llm_string) == [generation]
async def test_cache_still_empty(self, cache: BaseCache) -> None:
"""This test should follow a test that updates the cache.
This just verifies that the fixture is set up properly to be empty
after each test.
"""
assert (
await cache.alookup(self.get_sample_prompt(), self.get_sample_llm_string())
is None
)
async def test_clear_cache(self, cache: BaseCache) -> None:
"""Test clearing the cache."""
prompt = self.get_sample_prompt()
llm_string = self.get_sample_llm_string()
generation = self.get_sample_generation()
await cache.aupdate(prompt, llm_string, [generation])
await cache.aclear()
assert await cache.alookup(prompt, llm_string) is None
async def test_cache_miss(self, cache: BaseCache) -> None:
"""Test cache miss."""
assert (
await cache.alookup("Nonexistent prompt", self.get_sample_llm_string())
is None
)
async def test_cache_hit(self, cache: BaseCache) -> None:
"""Test cache hit."""
prompt = self.get_sample_prompt()
llm_string = self.get_sample_llm_string()
generation = self.get_sample_generation()
await cache.aupdate(prompt, llm_string, [generation])
assert await cache.alookup(prompt, llm_string) == [generation]
async def test_update_cache_with_multiple_generations(
self, cache: BaseCache
) -> None:
"""Test updating the cache with multiple Generation objects."""
prompt = self.get_sample_prompt()
llm_string = self.get_sample_llm_string()
generations = [
self.get_sample_generation(),
Generation(text="Another generated text."),
]
await cache.aupdate(prompt, llm_string, generations)
assert await cache.alookup(prompt, llm_string) == generations

View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
@ -605,6 +605,24 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras] [package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-asyncio"
version = "0.23.7"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
{file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
]
[package.dependencies]
pytest = ">=7.0.0,<9"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.1" version = "6.0.1"
@ -630,7 +648,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@ -780,4 +797,4 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.8.1,<4.0" python-versions = ">=3.8.1,<4.0"
content-hash = "a27025e6afa0275f153a9fd98c890c16187f9d01f5ca0b60aae23cee1a7d9dcc" content-hash = "fe8e04975482a0f8e67d07d186c401de1d321068dd1e595c836b156f7c4fcd9c"

View File

@ -21,6 +21,7 @@ optional = true
[tool.poetry.group.test.dependencies] [tool.poetry.group.test.dependencies]
langchain-core = { path = "../core", develop = true } langchain-core = { path = "../core", develop = true }
pytest-asyncio = "^0.23.7"
[tool.poetry.group.test_integration] [tool.poetry.group.test_integration]
optional = true optional = true
@ -60,3 +61,24 @@ omit = ["tests/*"]
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
# --strict-markers will raise errors on unknown marks.
# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
#
# https://docs.pytest.org/en/7.1.x/reference/reference.html
# --strict-config any warnings encountered while parsing the `pytest`
# section of the configuration file raise errors.
#
# https://github.com/tophat/syrupy
# --snapshot-warn-unused Prints a warning on unused snapshots rather than fail the test suite.
addopts = "--strict-markers --strict-config --durations=5 -vv"
# Registering custom markers.
# https://docs.pytest.org/en/7.1.x/example/markers.html#registering-markers
markers = [
"requires: mark tests as requiring a specific library",
"scheduled: mark tests to run in scheduled testing",
"compile: mark placeholder test used to compile integration tests without running them",
]
asyncio_mode = "auto"

View File

@ -0,0 +1,19 @@
import pytest
from langchain_core.caches import InMemoryCache
from langchain_standard_tests.integration_tests.cache import (
AsyncCacheTestSuite,
SyncCacheTestSuite,
)
class TestInMemoryCache(SyncCacheTestSuite):
@pytest.fixture
def cache(self) -> InMemoryCache:
return InMemoryCache()
class TestInMemoryCacheAsync(AsyncCacheTestSuite):
@pytest.fixture
async def cache(self) -> InMemoryCache:
return InMemoryCache()