115 lines
3.6 KiB
Python
115 lines
3.6 KiB
Python
"""测试基础设施:内存 SQLite(跳过索引)+ 内存 FakeRedis + 敏感词进程缓存复位"""
|
|
import pytest
|
|
from sqlalchemy import schema
|
|
from sqlalchemy.dialects import mysql
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
from sqlalchemy.ext.compiler import compiles
|
|
|
|
import app.models # noqa: F401 注册全部模型
|
|
from app.models.base import Base
|
|
from app.services import sensitive_service
|
|
|
|
|
|
@compiles(mysql.BIGINT, "sqlite")
|
|
def _bigint_as_integer(element, compiler, **kw):
|
|
# MySQL BIGINT UNSIGNED 在 SQLite 渲染为 INTEGER,主键才能成为 rowid 别名(自增可用)
|
|
return "INTEGER"
|
|
|
|
|
|
@compiles(mysql.INTEGER, "sqlite")
|
|
def _int_as_integer(element, compiler, **kw):
|
|
return "INTEGER"
|
|
|
|
|
|
class FakeRedis:
|
|
"""内存版 Redis 桩:覆盖搜索/敏感词/配置/限流用到的命令子集"""
|
|
|
|
def __init__(self):
|
|
self.kv: dict[str, str] = {}
|
|
self.hashes: dict[str, dict[str, str]] = {}
|
|
self.zsets: dict[str, dict[str, float]] = {}
|
|
self.ttls: dict[str, int] = {}
|
|
|
|
async def get(self, key):
|
|
return self.kv.get(key)
|
|
|
|
async def set(self, key, value, ex=None, nx=False):
|
|
if nx and key in self.kv:
|
|
return None
|
|
self.kv[key] = str(value)
|
|
if ex is not None:
|
|
self.ttls[key] = ex
|
|
return True
|
|
|
|
async def setex(self, key, ex, value):
|
|
self.kv[key] = str(value)
|
|
self.ttls[key] = ex
|
|
return True
|
|
|
|
async def delete(self, *keys):
|
|
for key in keys:
|
|
self.kv.pop(key, None)
|
|
self.hashes.pop(key, None)
|
|
self.zsets.pop(key, None)
|
|
return len(keys)
|
|
|
|
async def expire(self, key, ttl):
|
|
self.ttls[key] = ttl
|
|
return True
|
|
|
|
async def incr(self, key):
|
|
self.kv[key] = str(int(self.kv.get(key, "0")) + 1)
|
|
return int(self.kv[key])
|
|
|
|
async def hset(self, key, mapping=None, **kw):
|
|
items = mapping or kw
|
|
self.hashes.setdefault(key, {}).update({str(k): str(v) for k, v in items.items()})
|
|
return len(items)
|
|
|
|
async def hgetall(self, key):
|
|
return dict(self.hashes.get(key, {}))
|
|
|
|
async def hget(self, key, field):
|
|
return self.hashes.get(key, {}).get(field)
|
|
|
|
async def zadd(self, key, mapping):
|
|
self.zsets.setdefault(key, {}).update({str(m): float(s) for m, s in mapping.items()})
|
|
return len(mapping)
|
|
|
|
async def zincrby(self, key, amount, member):
|
|
z = self.zsets.setdefault(key, {})
|
|
z[str(member)] = z.get(str(member), 0.0) + float(amount)
|
|
return z[str(member)]
|
|
|
|
async def zrevrange(self, key, start, stop):
|
|
items = sorted(self.zsets.get(key, {}).items(), key=lambda kv: kv[1], reverse=True)
|
|
members = [m for m, _ in items]
|
|
if stop < 0:
|
|
stop = len(members) + stop # Redis 负索引语义
|
|
return members[start:stop + 1]
|
|
|
|
|
|
@pytest.fixture
|
|
def redis():
|
|
return FakeRedis()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_sensitive_cache():
|
|
# 敏感词匹配器进程缓存 60s,测试间必须复位
|
|
sensitive_service._local_cache = None
|
|
yield
|
|
sensitive_service._local_cache = None
|
|
|
|
|
|
@pytest.fixture
|
|
async def db():
|
|
"""内存 SQLite 会话:只建表不建索引(模型存在跨表重名索引,SQLite 索引名全局唯一)"""
|
|
eng = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
async with eng.begin() as conn:
|
|
await conn.run_sync(lambda c: [c.execute(schema.CreateTable(t)) for t in Base.metadata.sorted_tables])
|
|
Session = async_sessionmaker(eng, expire_on_commit=False)
|
|
async with Session() as s:
|
|
yield s
|
|
await eng.dispose()
|