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