"""测试基础设施:内存 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()