206 lines
5.6 KiB
Python
206 lines
5.6 KiB
Python
"""chongming-cache 双后端测试。
|
||||
|
|
|
|||
|
|
redis 后端优先连接真实服务(REDIS_HOST/REDIS_PORT),不可用时
|
|||
|
|
在进程内启动 fakeredis TCP 服务兜底,保证 redis 后端逻辑始终被验证。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import threading
|
|||
|
|
import time
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
from chongming_cache import Cache
|
|||
|
|
|
|||
|
|
REDIS_HOST = os.getenv("REDIS_HOST", "127.0.0.1")
|
|||
|
|
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
|
|||
|
|
REDIS_DB = int(os.getenv("REDIS_DB", 15)) # 测试专用库号,避免污染默认库
|
|||
|
|
|
|||
|
|
_server = None # fakeredis 兜底服务(进程内)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_redis_endpoint() -> tuple[str, int]:
|
|||
|
|
"""真实 redis 可用则用之;否则启动 fakeredis TCP 服务兜底。"""
|
|||
|
|
global _server
|
|||
|
|
import redis
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
client = redis.Redis(
|
|||
|
|
host=REDIS_HOST, port=REDIS_PORT, socket_connect_timeout=1
|
|||
|
|
)
|
|||
|
|
if client.ping():
|
|||
|
|
client.close()
|
|||
|
|
return REDIS_HOST, REDIS_PORT
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
import fakeredis
|
|||
|
|
|
|||
|
|
_server = fakeredis.TcpFakeServer(("127.0.0.1", 0), server_type="redis")
|
|||
|
|
port = _server.server_address[1]
|
|||
|
|
threading.Thread(target=_server.serve_forever, daemon=True).start()
|
|||
|
|
return "127.0.0.1", port
|
|||
|
|
|
|||
|
|
|
|||
|
|
_redis_host, _redis_port = _get_redis_endpoint()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture(scope="session", autouse=True)
|
|||
|
|
def _shutdown_fake_redis():
|
|||
|
|
"""会话结束时显式关闭 fakeredis 服务,避免其非 daemon 线程卡住解释器退出。"""
|
|||
|
|
yield
|
|||
|
|
if _server is not None:
|
|||
|
|
_server.shutdown()
|
|||
|
|
_server.server_close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def disk_cache(tmp_path):
|
|||
|
|
cache = Cache.from_config(
|
|||
|
|
{"backend": "diskcache", "cache_store_path": str(tmp_path / "cache")}
|
|||
|
|
)
|
|||
|
|
yield cache
|
|||
|
|
cache.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def redis_cache():
|
|||
|
|
"""redis 后端实例(唯一前缀命名空间,测试结束只清自己的 key)。"""
|
|||
|
|
cache = Cache.from_config(
|
|||
|
|
{
|
|||
|
|
"backend": "redis",
|
|||
|
|
"redis_host": _redis_host,
|
|||
|
|
"redis_port": _redis_port,
|
|||
|
|
"redis_db": REDIS_DB,
|
|||
|
|
"redis_prefix": f"test:cache:{os.getpid()}:",
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
yield cache
|
|||
|
|
cache.clear()
|
|||
|
|
cache.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _mapping_checks(cache: Cache) -> None:
|
|||
|
|
cache["k1"] = {"a": 1}
|
|||
|
|
cache.set("k2", [1, 2, 3])
|
|||
|
|
cache.set("k3", "bytes-able", expire=60)
|
|||
|
|
|
|||
|
|
assert "k1" in cache
|
|||
|
|
assert cache["k1"] == {"a": 1}
|
|||
|
|
assert cache["k2"] == [1, 2, 3]
|
|||
|
|
assert cache.get("k3") == "bytes-able"
|
|||
|
|
assert cache.get("missing") is None
|
|||
|
|
assert cache.get("missing", "d") == "d"
|
|||
|
|
with pytest.raises(KeyError):
|
|||
|
|
cache["missing"]
|
|||
|
|
assert len(cache) == 3
|
|||
|
|
assert set(cache) == {"k1", "k2", "k3"}
|
|||
|
|
|
|||
|
|
del cache["k1"]
|
|||
|
|
assert "k1" not in cache
|
|||
|
|
with pytest.raises(KeyError):
|
|||
|
|
del cache["k1"]
|
|||
|
|
assert cache.delete("k2") is True
|
|||
|
|
assert cache.delete("k2") is False
|
|||
|
|
assert len(cache) == 1
|
|||
|
|
|
|||
|
|
cache.clear()
|
|||
|
|
assert len(cache) == 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_mapping_disk(disk_cache):
|
|||
|
|
_mapping_checks(disk_cache)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_expire_disk(disk_cache):
|
|||
|
|
disk_cache.set("e", 1, expire=1)
|
|||
|
|
assert "e" in disk_cache
|
|||
|
|
time.sleep(1.2)
|
|||
|
|
assert "e" not in disk_cache # diskcache 读时惰性淘汰
|
|||
|
|
assert disk_cache.get("e", "miss") == "miss"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_touch_disk(disk_cache):
|
|||
|
|
disk_cache.set("e", 1, expire=1)
|
|||
|
|
assert disk_cache.touch("e", expire=10) is True
|
|||
|
|
time.sleep(1.2)
|
|||
|
|
assert "e" in disk_cache
|
|||
|
|
assert disk_cache.touch("missing", expire=10) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_context_manager(tmp_path):
|
|||
|
|
with Cache.from_config(
|
|||
|
|
{"backend": "diskcache", "cache_store_path": str(tmp_path / "cm")}
|
|||
|
|
) as cache:
|
|||
|
|
cache["x"] = 1
|
|||
|
|
assert cache["x"] == 1
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_from_config_defaults(tmp_path, monkeypatch):
|
|||
|
|
# 缺省 backend 时默认 diskcache;缺省 cache_store_path 时用 ./cache_data
|
|||
|
|
monkeypatch.chdir(tmp_path)
|
|||
|
|
cache = Cache.from_config({})
|
|||
|
|
try:
|
|||
|
|
cache.set("k", 1)
|
|||
|
|
assert cache["k"] == 1
|
|||
|
|
finally:
|
|||
|
|
cache.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_invalid_backend(tmp_path):
|
|||
|
|
with pytest.raises(ValueError):
|
|||
|
|
Cache.from_config({"backend": "memcached", "cache_store_path": str(tmp_path)})
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── redis 后端 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_mapping_redis(redis_cache):
|
|||
|
|
_mapping_checks(redis_cache)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_expire_redis(redis_cache):
|
|||
|
|
redis_cache.set("e", 1, expire=1)
|
|||
|
|
assert "e" in redis_cache
|
|||
|
|
time.sleep(1.2)
|
|||
|
|
assert "e" not in redis_cache # TTL 到期后 EXISTS 为假
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_touch_redis(redis_cache):
|
|||
|
|
redis_cache.set("e", 1, expire=1)
|
|||
|
|
assert redis_cache.touch("e", expire=10) is True
|
|||
|
|
time.sleep(1.2)
|
|||
|
|
assert "e" in redis_cache
|
|||
|
|
assert redis_cache.touch("missing", expire=10) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_prefix_isolation():
|
|||
|
|
"""不同前缀实例共享同一 Redis,clear 只清自己的 key。"""
|
|||
|
|
base = {
|
|||
|
|
"backend": "redis",
|
|||
|
|
"redis_host": _redis_host,
|
|||
|
|
"redis_port": _redis_port,
|
|||
|
|
"redis_db": REDIS_DB,
|
|||
|
|
}
|
|||
|
|
a = Cache.from_config({**base, "redis_prefix": f"test:iso:a:{os.getpid()}:"})
|
|||
|
|
b = Cache.from_config({**base, "redis_prefix": f"test:iso:b:{os.getpid()}:"})
|
|||
|
|
try:
|
|||
|
|
a["x"] = 1
|
|||
|
|
b["y"] = 2
|
|||
|
|
a.clear()
|
|||
|
|
assert "x" not in a and "y" in b
|
|||
|
|
assert len(a) == 0 and len(b) == 1
|
|||
|
|
assert set(b) == {"y"}
|
|||
|
|
finally:
|
|||
|
|
a.clear()
|
|||
|
|
b.clear()
|
|||
|
|
a.close()
|
|||
|
|
b.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_redis_binary_value_roundtrip(redis_cache):
|
|||
|
|
"""非 pickle 可序列化值应正常往返(dict/bytes 等)。"""
|
|||
|
|
value = {"nested": {"list": [1, 2, 3]}, "bytes": b"\x00\x01"}
|
|||
|
|
redis_cache["k"] = value
|
|||
|
|
assert redis_cache["k"] == value
|