muse-agent-example/tests/单元/test_数据库底座生命周期.py

295 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""不连接 PG,验证模板失败、复用及清理所有权的生命周期。"""
from pathlib import Path
import pytest
from 支持.数据库底座 import 库身份, 数据库底座
class 环境替身:
def __init__(self):
self.库 = {}
self.迁移次数 = 0
self.角色次数 = 0
self.序号 = 0
self.失败迁移 = False
self.拒绝清理 = set()
def 初始化角色(self, 定义):
self.角色次数 += 1
return True
def 环境身份(self):
return {"major": 16, "extensions": [["plpgsql", "1.0"]]}
def 创建(self, 名称, 注记, 模板=None):
if 模板:
assert self.库[模板.名称][0] == 模板
if not self.库[模板.名称][1]:
raise RuntimeError("模板未封存")
self.序号 += 1
身份 = 库身份(名称, self.序号, 42, 注记)
self.库[名称] = (身份, False)
return 身份
def 迁移(self, 身份, 目录, 引用目录):
self.迁移次数 += 1
if self.失败迁移:
raise RuntimeError("迁移失败")
return len(list(目录.glob("*.sql")))
def 封存(self, 身份):
self.库[身份.名称] = (身份, True)
def 删除(self, 身份):
if 身份.名称 in self.拒绝清理:
raise RuntimeError("连接仍占用")
if self.库[身份.名称][0] != 身份:
raise RuntimeError("身份漂移")
del self.库[身份.名称]
def 工厂(self, 名称, 目录):
return {"database": 名称}
def 清空业务数据(self, 身份):
self.清空次数 = getattr(self, "清空次数", 0) + 1
@pytest.fixture
def 环境(tmp_path):
根 = tmp_path / "repo"
(根 / "数据库/初始化").mkdir(parents=True)
(根 / "数据库/初始化/用途角色.sql").write_text("SELECT 1;")
(根 / "数据库/迁移").mkdir()
for 版本 in (1, 2, 4, 16):
(根 / f"数据库/迁移/V{版本:04d}__test.sql").write_text(f"SELECT {版本};")
后端 = 环境替身()
底座 = 数据库底座(后端, 根, tmp_path / "baseline")
yield 底座, 后端, 根
if 底座.存活:
后端.拒绝清理.clear()
底座.关闭()
@pytest.mark.case_id("NC-database-baseline-unit-01")
def test_同基线多例只迁移一次且逐例独立释放(环境, tmp_path):
底座, 后端, _ = 环境
名称 = []
for 行 in range(3):
with 底座.借库(tmp_path / str(行), (1, 4, 16)) as 工厂:
名称.append(工厂["database"])
assert len(后端.库) == 2
assert len(后端.库) == 1
assert len(set(名称)) == 3
assert 后端.迁移次数 == 后端.角色次数 == 1
assert [e["files"] for e in 底座.事件 if e["action"] == "ddl"] == [3]
底座.关闭()
assert 后端.库 == {}
@pytest.mark.case_id("NC-database-baseline-unit-02")
def test_轻量端点与空库不预执行迁移(环境, tmp_path):
底座, 后端, _ = 环境
assert 底座.端点(tmp_path)["database"] is None
assert not 后端.库
with 底座.借库(tmp_path, 空库=True):
assert len(后端.库) == 1
assert 后端.迁移次数 == 0
assert not 底座.模板
@pytest.mark.case_id("NC-database-baseline-unit-03")
def test_种子变体克隆普通模板且只导入一次(环境, tmp_path):
底座, 后端, _ = 环境
导入库 = []
for _ in range(2):
with 底座.借库(
tmp_path, 种子指纹="seed-1", 导入种子=lambda 工厂: 导入库.append(工厂["database"])
):
pass
assert 后端.迁移次数 == 1
assert len(导入库) == 1
assert len(底座.模板) == 2
@pytest.mark.case_id("NC-database-baseline-unit-04")
def test_构建失败删除不完整模板并可重试(环境):
底座, 后端, _ = 环境
后端.失败迁移 = True
with pytest.raises(RuntimeError, match="迁移失败"):
底座.基线()
assert not 底座.模板 and not 后端.库
后端.失败迁移 = False
底座.基线()
assert len(底座.模板) == 1
@pytest.mark.case_id("NC-database-baseline-unit-05")
def test_测试异常仍清理而且不会删其他worker(环境, tmp_path):
底座, 后端, 根 = 环境
另一个 = 数据库底座(后端, 根, tmp_path / "worker2", worker="gw1")
另一个.基线()
with pytest.raises(ValueError), 底座.借库(tmp_path):
raise ValueError("调用失败")
底座.关闭()
assert len(后端.库) == 1
assert next(iter(后端.库)) == next(iter(另一个.存活))
另一个.关闭()
@pytest.mark.case_id("NC-database-baseline-unit-06")
def test_源码改变拒绝复用旧指纹(环境):
底座, 后端, 根 = 环境
底座.基线()
(根 / "数据库/迁移/V0001__test.sql").write_text("SELECT 100;")
with pytest.raises(RuntimeError, match="已改变"):
底座.基线()
assert 后端.迁移次数 == 1
@pytest.mark.case_id("NC-database-baseline-unit-07")
def test_未封存模板不能克隆(环境, tmp_path):
底座, 后端, _ = 环境
模板 = 底座.基线()
后端.库[模板.名称] = (模板, False)
with pytest.raises(RuntimeError, match="未封存"), 底座.借库(tmp_path):
pass
assert len(后端.库) == 1
@pytest.mark.case_id("NC-database-baseline-unit-08")
def test_失败清理保留精确对象且继续清理其他库(环境):
底座, 后端, _ = 环境
一 = 底座.基线((1,))
二 = 底座.基线((1, 2))
后端.拒绝清理.add(二.名称)
with pytest.raises(ExceptionGroup):
底座.关闭()
assert 一.名称 not in 后端.库
assert list(底座.存活) == [二.名称]
assert any(e["action"] == "cleanup_failed" and e["database"] == 二.名称 for e in 底座.事件)
@pytest.mark.case_id("NC-database-baseline-unit-09")
def test_删除未登记身份被拒绝(环境):
底座, _, _ = 环境
with pytest.raises(RuntimeError, match="未登记"):
底座._删除(库身份("foreign", 1, 42, "another-run"))
@pytest.mark.case_id("NC-database-baseline-unit-10")
def test_真实后端核验拒绝同名换库与活模板连接(tmp_path):
from 支持.数据库底座 import PostgreSQL后端
class 管理连接:
def __init__(self, 数据库行, 活连接=0):
self.数据库行, self.活连接 = 数据库行, 活连接
def execute(self, SQL, 参数):
self.结果 = (self.活连接,) if "pg_stat_activity" in SQL else self.数据库行
return self
def fetchone(self):
return self.结果
后端 = PostgreSQL后端("synthetic-no-connect", tmp_path)
身份 = 库身份("case", 10, 42, "current-session")
for 行 in [None, (11, 42, "current-session", False), (10, 42, "foreign", False)]:
with pytest.raises(RuntimeError, match="身份已改变"):
后端._核验(管理连接(行), 身份)
with pytest.raises(RuntimeError, match="未封存或仍有连接"):
后端._核验(管理连接((10, 42, "current-session", False), 活连接=1), 身份, 模板=True)
后端._核验(管理连接((10, 42, "current-session", False)), 身份, 模板=True)
@pytest.mark.case_id("NC-database-baseline-unit-11")
def test_同次运行多worker角色只初始化一次且失败可重试(monkeypatch, tmp_path):
from 支持 import 数据库底座 as 模块
class 管理连接:
def __init__(self):
self.执行 = []
self.失败 = True
def __enter__(self):
return self
def __exit__(self, *参数):
pass
def execute(self, SQL):
self.执行.append(SQL)
if SQL == "roles" and self.失败:
raise RuntimeError("角色失败")
连 = 管理连接()
monkeypatch.setattr(模块.psycopg, "connect", lambda *a, **k: 连)
一 = 模块.PostgreSQL后端("synthetic", tmp_path)
二 = 模块.PostgreSQL后端("synthetic", tmp_path)
with pytest.raises(RuntimeError, match="角色失败"):
一.初始化角色("roles")
连.失败 = False
assert 一.初始化角色("roles") is True
assert 二.初始化角色("roles") is False
assert 连.执行.count("roles") == 2 # 一次失败,一次成功;后续worker没有重放。
@pytest.mark.case_id("NC-database-baseline-unit-12")
def test_连接引用初次创建即限制权限且不回写已有文件(tmp_path):
import stat
from muse.共享.调用身份 import 用途
from 支持.数据库底座 import PostgreSQL后端
后端 = PostgreSQL后端("host=synthetic dbname=admin user=admin", tmp_path / "roles")
工厂 = 后端.工厂("owned-clone", tmp_path / "references")
assert set(工厂) == {用途.维护, 用途.生产, 用途.评测}
for 项 in 工厂.values():
路径 = Path(项.引用.位置)
assert stat.S_IMODE(路径.stat().st_mode) == 0o600
assert "dbname=owned-clone" in 路径.read_text()
with pytest.raises(FileExistsError):
后端.工厂("another-clone", tmp_path / "references")
@pytest.mark.case_id("NC-database-baseline-unit-13")
def test_种子导入失败只清理派生模板且允许重试(环境):
底座, 后端, _ = 环境
def 失败(工厂):
raise ValueError("种子失败")
with pytest.raises(ValueError, match="种子失败"):
底座.基线(种子指纹="seed-v1", 导入种子=失败)
assert 后端.迁移次数 == 1
assert len(后端.库) == len(底座.模板) == 1
底座.基线(种子指纹="seed-v1", 导入种子=lambda 工厂: None)
assert 后端.迁移次数 == 1
assert len(后端.库) == len(底座.模板) == 2
@pytest.mark.case_id("NC-database-baseline-unit-14")
def test_已关闭底座拒绝复用旧模板(环境, tmp_path):
底座, 后端, _ = 环境
底座.基线()
底座.关闭()
with pytest.raises(RuntimeError, match="已关闭"), 底座.借库(tmp_path):
pass
assert not 后端.库
@pytest.mark.case_id("NC-database-baseline-unit-15")
def test_共享库同指纹复用同一库且例间清空业务数据(环境, tmp_path):
底座, 后端, _ = 环境
名称 = []
for 行 in range(3):
with 底座.共享库(tmp_path / str(行), (1, 4, 16)) as 工厂:
名称.append(工厂["database"])
assert len(后端.库) == 2 # 1个模板 + 1个共享库
assert len(后端.库) == 2 # 例后不销毁共享库
assert len(set(名称)) == 1 # 全部复用同一个库名
assert getattr(后端, "清空次数", 0) == 3 # 每次例后触发清空
底座.关闭()
assert 后端.库 == {} # 关闭时清理全部库