add
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""测试基础设施:内存 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()
|
||||
Reference in New Issue
Block a user