295 lines
10 KiB
Python
295 lines
10 KiB
Python
"""不连接 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 后端.库 == {} # 关闭时清理全部库
|