# -*- coding: utf-8 -*- """M11b-2 单事务写入 —— 可执行单元测试(sqlite 文件库真实跑通,非 import 面检查)。 覆盖需求点与验收标准: - 正常写入:事件 + 状态在同一事务落库,两表各 1 行(TestAtomicWriteSuccess); - 事件插入失败 → 状态更新一并回滚(TestRollbackOnEventFailure); - 状态更新失败/并发冲突 → 整体回滚,事件不留(TestRollbackOnStateFailure); - 并发:多事务写同一实体,不出现脏写/丢失更新(TestConcurrencyNoLostUpdate); - 租户/世界隔离:跨租户同名 entity_id 互不影响(TestTenantWorldIsolation); - P99 ≤ 200ms(TestLatencyP99,percentile 与 m11b-selftest-run.log 同口径); - conn_factory 三级优先级 + fail-closed + 禁读 env/*.json(TestConnFactoryResolution); - 接线面:导出一致性、禁项(无广播/轮询)扫描(TestPackageExports / TestNoForbiddenLogic)。 运行:cd apps/scense/pkgs/world_sync && python -m pytest tests/test_m11b2_wiring.py -v """ import os import sqlite3 import sys import threading import pytest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from world_sync import pbl_runtime_sql as ps # noqa: E402 from world_sync.pbl_runtime_errors import ( # noqa: E402 ConcurrentStateConflict, ConnFactoryNotConfigured, EventWriteError, PblRuntimeError, StateWriteError, TxConfigError, ) from world_sync.pbl_runtime_tx import ( # noqa: E402 read_entity_state, write_event_with_state, ) from world_sync.pbl_runtime_tx_env import ( # noqa: E402 build_sqlite_conn_factory, resolve_conn_factory, ) percentile = ps.percentile # -------------------------------------------------------------------------- # fixtures / helpers # -------------------------------------------------------------------------- @pytest.fixture def db_path(tmp_path): """文件型 sqlite 库(多线程共享同一库,比 :memory: 更适合并发用例)。""" return str(tmp_path / "m11b2.db") @pytest.fixture def factory(db_path): """sqlite 连接工厂,并保证两表 DDL 已建(CREATE TABLE IF NOT EXISTS 幂等)。""" f = build_sqlite_conn_factory(db_path) c = f() for stmt in ps.ddl_for(ps.DIALECT_SQLITE): c.execute(stmt) c.commit() c.close() return f @pytest.fixture def conn(factory): c = factory() yield c c.close() def _event(tenant="t1", world="w1", entity="e1", **kw): base = { "tenant_id": tenant, "world_id": world, "entity_id": entity, "event_type": "state.updated", "payload": {"hp": 100}, "session_id": "s-1", "source": "unit-test", } base.update(kw) return base def _state(tenant="t1", world="w1", entity="e1", **kw): base = {"tenant_id": tenant, "world_id": world, "entity_id": entity, "state": {"hp": 100, "x": 1}} base.update(kw) return base def _counts(conn): ev = conn.execute("SELECT COUNT(*) FROM pbl_runtime_event").fetchone()[0] st = conn.execute("SELECT COUNT(*) FROM pbl_entity_state").fetchone()[0] return ev, st # -------------------------------------------------------------------------- # 1. 正常写入:单事务内事件 + 状态各 1 行 # -------------------------------------------------------------------------- class TestAtomicWriteSuccess(object): def test_first_write_inserts_state_and_event(self, conn, factory): result = write_event_with_state(_event(), _state(state={"hp": 90}), conn_factory=factory) assert result["action"] == "inserted" assert result["state_version"] == 1 assert result["dialect"] == "sqlite" assert _counts(conn) == (1, 1) row = conn.execute( "SELECT event_id, entity_id, state_version, payload, created_at " "FROM pbl_runtime_event" ).fetchone() assert row[1] == "e1" assert row[2] == 1 assert '"hp":100' in row[3] # payload 存事件体 assert row[4].endswith("Z") st = conn.execute( "SELECT state, state_version, updated_by_event FROM pbl_entity_state" ).fetchone() assert '"hp":90' in st[0] # 状态存新值 assert st[1] == 1 assert st[2] == row[0] # 状态行回指生效事件 def test_second_write_bumps_version(self, conn, factory): write_event_with_state(_event(), _state(state={"hp": 90}), conn_factory=factory) r2 = write_event_with_state(_event(), _state(state={"hp": 70}), conn_factory=factory) assert r2["action"] == "updated" assert r2["state_version"] == 2 assert _counts(conn) == (2, 1) def test_event_only_write_keeps_state(self, conn, factory): write_event_with_state(_event(), _state(state={"hp": 90}), conn_factory=factory) r = write_event_with_state(_event(event_type="chat"), None, conn_factory=factory) assert r["action"] == "event_only" assert _counts(conn) == (2, 1) def test_read_entity_state_helper(self, factory): write_event_with_state(_event(), _state(state={"hp": 42}), conn_factory=factory) got = read_entity_state(conn_factory=factory, tenant_id="t1", world_id="w1", entity_id="e1") assert got["state_version"] == 1 assert '"hp":42' in got["state"] assert read_entity_state(conn_factory=factory, tenant_id="t1", world_id="w1", entity_id="nope") is None def test_missing_tenant_rejected_before_touching_db(self, factory): with pytest.raises(TxConfigError) as ei: write_event_with_state({"world_id": "w1", "entity_id": "e1"}, _state(), conn_factory=factory) assert ei.value.code == "PBL_TX_CONFIG_ERROR" assert ei.value.detail.get("field") == "tenant_id" assert _counts(factory()) == (0, 0) def test_conflicting_scope_rejected(self, factory): with pytest.raises(TxConfigError): write_event_with_state(_event(tenant="t1"), _state(tenant="t2"), conn_factory=factory) def test_result_is_serializable(self, factory): import json r = write_event_with_state(_event(), _state(), conn_factory=factory) json.dumps(r) # 不抛即通过 # -------------------------------------------------------------------------- # 2. 失败整体回滚:事件插入失败 → 状态变更消失 # -------------------------------------------------------------------------- class TestRollbackOnEventFailure(object): def test_event_failure_rolls_back_state_update(self, conn, factory): """状态先更新成功,事件插入被触发器打坏 → 断言两表零增量、状态仍是旧值。""" write_event_with_state(_event(), _state(state={"hp": 90}), conn_factory=factory) conn.execute( "CREATE TRIGGER fail_event BEFORE INSERT ON pbl_runtime_event " "BEGIN SELECT RAISE(ABORT, 'boom: event insert failed'); END" ) with pytest.raises(EventWriteError) as ei: write_event_with_state(_event(), _state(state={"hp": 1}), conn_factory=factory) assert ei.value.code == "PBL_RUNTIME_EVENT_WRITE_FAILED" assert "boom" in str(ei.value) assert _counts(conn) == (1, 1) st = conn.execute("SELECT state, state_version FROM pbl_entity_state").fetchone() assert '"hp":90' in st[0] assert st[1] == 1 def test_duplicate_event_id_rolls_back_state(self, conn, factory): """同 event_id 主键冲突 → 事件失败 → 状态更新回滚。""" write_event_with_state(_event(), _state(state={"hp": 90}), conn_factory=factory, event_id="dup-1") with pytest.raises(EventWriteError): write_event_with_state(_event(), _state(state={"hp": 5}), conn_factory=factory, event_id="dup-1") assert _counts(conn) == (1, 1) st = conn.execute("SELECT state_version FROM pbl_entity_state").fetchone() assert st[0] == 1 # -------------------------------------------------------------------------- # 3. 失败整体回滚:状态更新失败 / 并发冲突 # -------------------------------------------------------------------------- class TestRollbackOnStateFailure(object): def test_expected_version_conflict_rolls_back_both(self, conn, factory): write_event_with_state(_event(), _state(state={"hp": 90}), conn_factory=factory) with pytest.raises(ConcurrentStateConflict) as ei: write_event_with_state( _event(), _state(state={"hp": 0}, expected_version=99), conn_factory=factory) assert ei.value.code == "PBL_ENTITY_STATE_CONFLICT" assert ei.value.detail["expected_version"] == 99 assert ei.value.detail["actual_version"] == 1 assert _counts(conn) == (1, 1) # 冲突时事件一条都不能多留 def test_state_trigger_failure_rolls_back_event(self, conn, factory): """状态 UPDATE 失败(触发器报错)→ 本事务的事件也不能落库。""" write_event_with_state(_event(), _state(state={"hp": 90}), conn_factory=factory) conn.execute( "CREATE TRIGGER fail_state BEFORE UPDATE ON pbl_entity_state " "BEGIN SELECT RAISE(ABORT, 'boom: state update failed'); END" ) with pytest.raises(StateWriteError): write_event_with_state(_event(), _state(state={"hp": 3}), conn_factory=factory) # 事件仍只有首条(失败事务的事件被回滚),状态版本仍为 1 assert _counts(conn) == (1, 1) assert conn.execute( "SELECT state_version FROM pbl_entity_state").fetchone()[0] == 1 def test_failed_write_leaves_no_open_transaction(self, factory): """失败后连接必须已回滚,后续写入不受影响(不吞错误、不留半截事务)。""" c = factory() c.execute( "CREATE TRIGGER fail_event BEFORE INSERT ON pbl_runtime_event " "BEGIN SELECT RAISE(ABORT, 'boom'); END" ) with pytest.raises(EventWriteError): write_event_with_state(_event(), _state(), conn=c) c.execute("DROP TRIGGER fail_event") r = write_event_with_state(_event(), _state(), conn=c) assert r["state_version"] == 1 assert c.execute("SELECT COUNT(*) FROM pbl_runtime_event").fetchone()[0] == 1 assert c.execute("SELECT COUNT(*) FROM pbl_entity_state").fetchone()[0] == 1 c.close() # -------------------------------------------------------------------------- # 4. 并发:不出现脏写 / 丢失更新 # -------------------------------------------------------------------------- class TestConcurrencyNoLostUpdate(object): def test_concurrent_writers_no_lost_update(self, factory): """8 线程同时写同一实体:成功版本号连续无空洞,事件数 == 成功数。""" threads = 8 results = [] errors = [] lock = threading.Lock() barrier = threading.Barrier(threads) def worker(i): barrier.wait() try: r = write_event_with_state( _event(payload={"writer": i}), _state(state={"hp": 100 + i}), conn_factory=factory, ) with lock: results.append(r["state_version"]) except PblRuntimeError as exc: with lock: errors.append(exc.code) except sqlite3.OperationalError as exc: # sqlite 文件库锁超时属环境限制,不算脏写 with lock: errors.append("locked:%s" % str(exc)[:40]) ts = [threading.Thread(target=worker, args=(i,)) for i in range(threads)] for t in ts: t.start() for t in ts: t.join() c = factory() try: n_events = c.execute("SELECT COUNT(*) FROM pbl_runtime_event").fetchone()[0] st = c.execute("SELECT state_version FROM pbl_entity_state").fetchone() finally: c.close() assert len(results) >= 1, "all writers failed: %s" % errors assert n_events == len(results) # 没有「事件写了但状态没推进」的半截事务 assert st[0] == len(results) # 版本 == 成功提交数,无丢失更新 assert sorted(results) == list(range(1, len(results) + 1)) # 连续无空洞 assert not [e for e in errors if not e.startswith("locked")] def test_optimistic_lock_rejects_stale_writer(self, conn, factory): """显式 expected_version 的写者:版本被别人推进后必须被拒绝(不覆盖)。""" write_event_with_state(_event(), _state(state={"hp": 90}), conn_factory=factory) write_event_with_state(_event(), _state(state={"hp": 80}), conn_factory=factory) with pytest.raises(ConcurrentStateConflict): write_event_with_state(_event(), _state(state={"hp": 70}, expected_version=1), conn_factory=factory) assert _counts(conn) == (2, 1) st = conn.execute("SELECT state FROM pbl_entity_state").fetchone() assert '"hp":80' in st[0] # 迟到的旧版本写者没有覆盖新状态 # -------------------------------------------------------------------------- # 5. 租户 / 世界隔离 # -------------------------------------------------------------------------- class TestTenantWorldIsolation(object): def test_same_entity_id_across_tenants_isolated(self, conn, factory): write_event_with_state(_event(tenant="t1"), _state(tenant="t1", state={"hp": 1}), conn_factory=factory) write_event_with_state(_event(tenant="t2"), _state(tenant="t2", state={"hp": 2}), conn_factory=factory) assert _counts(conn) == (2, 2) r = conn.execute( "SELECT tenant_id, state_version FROM pbl_entity_state ORDER BY tenant_id" ).fetchall() assert r == [("t1", 1), ("t2", 1)] # 各自独立版本,互不覆盖 def test_same_entity_id_across_worlds_isolated(self, conn, factory): write_event_with_state(_event(world="w1"), _state(world="w1"), conn_factory=factory) write_event_with_state(_event(world="w2"), _state(world="w2"), conn_factory=factory) assert _counts(conn) == (2, 2) def test_event_query_scoped_by_tenant(self, conn, factory): write_event_with_state(_event(tenant="t1"), _state(tenant="t1"), conn_factory=factory) write_event_with_state(_event(tenant="t2"), _state(tenant="t2"), conn_factory=factory) n = conn.execute( "SELECT COUNT(*) FROM pbl_runtime_event WHERE tenant_id = ? AND world_id = ?", ("t1", "w1")).fetchone()[0] assert n == 1 # -------------------------------------------------------------------------- # 6. 时延:P99 ≤ 200ms(不含广播) # -------------------------------------------------------------------------- class TestLatencyP99(object): def test_percentile_linear_interpolation(self): assert percentile([], 99) == 0.0 assert percentile([5], 99) == 5.0 assert percentile([1, 2, 3, 4], 50) == 2.5 assert abs(percentile(list(range(1, 101)), 99) - 99.01) < 1e-9 assert percentile([10, 20, 30], 0) == 10 assert percentile([10, 20, 30], 100) == 30 def test_single_tx_p99_under_200ms(self, factory): """100 次单事务写入(事件+状态),P99 ≤ 200ms(sqlite 本地口径)。""" samples = [] for i in range(100): r = write_event_with_state( _event(entity="perf-%d" % (i % 10), payload={"i": i}), _state(entity="perf-%d" % (i % 10), state={"i": i}), conn_factory=factory, ) samples.append(r["elapsed_ms"]) p99 = percentile(samples, 99) assert p99 <= 200.0, "P99=%.3fms exceeds 200ms budget" % p99 assert percentile(samples, 50) <= 200.0 # -------------------------------------------------------------------------- # 7. conn_factory 三级优先级 + fail-closed + 禁读 env/*.json # -------------------------------------------------------------------------- class TestConnFactoryResolution(object): def test_explicit_factory_wins(self, tmp_path, monkeypatch): path = str(tmp_path / "explicit.db") called = [] real = build_sqlite_conn_factory(path) def counting(): called.append(1) return real() monkeypatch.setenv("PBL_RUNTIME_DB_URL", "sqlite:///%s" % (tmp_path / "env.db")) f = resolve_conn_factory(counting) c = f() c.execute("SELECT 1").fetchone() c.close() assert called == [1] def test_env_url_used_when_no_injection(self, tmp_path, monkeypatch): path = str(tmp_path / "from-env.db") monkeypatch.setenv("PBL_RUNTIME_DB_URL", "sqlite:///%s" % path) f = resolve_conn_factory(None) c = f() assert c is not None c.close() assert os.path.isfile(path) def test_db_url_string_injection(self, tmp_path, monkeypatch): path = str(tmp_path / "by-string.db") f = resolve_conn_factory("sqlite:///%s" % path) c = f() c.close() assert os.path.isfile(path) def test_fail_closed_when_nothing_configured(self, monkeypatch): import world_sync.pbl_runtime_tx_env as txenv monkeypatch.delenv("PBL_RUNTIME_DB_URL", raising=False) monkeypatch.delenv("WORLD_SYNC_DB_URL", raising=False) # 屏蔽模块自有 conf/db.json,模拟「三级全空」 monkeypatch.setattr(txenv, "load_db_config", lambda conf_dir=None: None) with pytest.raises(ConnFactoryNotConfigured) as ei: resolve_conn_factory(None) assert ei.value.code == "PBL_TX_CONN_FACTORY_MISSING" assert "forbidden" in str(ei.value) assert ei.value.detail["tried"] == [ "explicit", "server_env", "env_vars", "module_conf/db.json"] def test_plaintext_password_in_module_conf_rejected(self, monkeypatch): import world_sync.pbl_runtime_tx_env as txenv monkeypatch.setattr( txenv, "load_db_config", lambda conf_dir=None: {"dialect": "mysql", "host": "h", "user": "u", "database": "d", "password": "s3cr3t"}) with pytest.raises(TxConfigError): txenv._from_module_conf() def test_unsupported_url_scheme_rejected(self): import world_sync.pbl_runtime_tx_env as txenv with pytest.raises(TxConfigError): txenv._parse_db_url("oracle://x/y") def test_no_env_test_json_candidate_in_source(self): """QC #5 回归守卫:源码里不得再出现 env/test.json 之类的凭据文件候选路径。""" root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) pkg = os.path.join(root, "world_sync") banned = ('"env/test.json"', '"env/prod.json"', "'env/test.json'", "'env/prod.json'", "projects/pbls/env") for name in sorted(os.listdir(pkg)): if not name.endswith(".py"): continue with open(os.path.join(pkg, name), "r", encoding="utf-8") as fh: text = fh.read() for token in banned: assert token not in text, "%s still references %s" % (name, token) # -------------------------------------------------------------------------- # 8. 方言层单测 # -------------------------------------------------------------------------- class TestDialectLayer(object): def test_detect_sqlite(self, conn): assert ps.detect_dialect(conn) == ps.DIALECT_SQLITE def test_placeholders_per_dialect(self): assert ps.placeholder(ps.DIALECT_SQLITE, 0) == "?" assert ps.placeholder(ps.DIALECT_MYSQL, 0) == "%s" assert ps.placeholder(ps.DIALECT_POSTGRES, 2) == "$3" assert ps.render_placeholders(ps.DIALECT_MYSQL, 3) == "%s, %s, %s" def test_for_update_only_on_row_lock_dbs(self): assert ps.for_update_clause(ps.DIALECT_SQLITE) == "" assert ps.for_update_clause(ps.DIALECT_MYSQL).strip() == "FOR UPDATE" assert ps.for_update_clause(ps.DIALECT_POSTGRES).strip() == "FOR UPDATE" def test_read_committed_stmt(self): assert "READ COMMITTED" in ps.read_committed_stmt(ps.DIALECT_MYSQL) assert "READ COMMITTED" in ps.read_committed_stmt(ps.DIALECT_POSTGRES) assert ps.read_committed_stmt(ps.DIALECT_SQLITE) is None def test_begin_stmt(self): assert ps.begin_stmt(ps.DIALECT_SQLITE) == "BEGIN IMMEDIATE" assert ps.begin_stmt(ps.DIALECT_MYSQL) is None def test_unknown_dialect_raises(self): class Weird(object): pass with pytest.raises(ValueError): ps.detect_dialect(Weird()) def test_ddl_covers_both_tables(self): text = " ".join(ps.ddl_for(ps.DIALECT_SQLITE)) assert ps.EVENT_TABLE in text and ps.STATE_TABLE in text assert "PRIMARY KEY (tenant_id, world_id, entity_id)" in text assert "ENGINE=InnoDB" in " ".join(ps.ddl_for(ps.DIALECT_MYSQL)) def test_no_ddl_for_unknown_dialect(self): with pytest.raises(ValueError): ps.ddl_for("oracle") # -------------------------------------------------------------------------- # 9. 接线面 / 禁项扫描 # -------------------------------------------------------------------------- class TestPackageExports(object): def test_all_is_importable(self): import world_sync as ws for name in ws.__all__: assert hasattr(ws, name), "missing export: %s" % name def test_public_api_surface(self): import world_sync as ws assert callable(ws.write_event_with_state) assert callable(ws.read_entity_state) assert "ConcurrentStateConflict" in ws.__all__ assert "EventWriteError" in ws.__all__ def test_sqlor_layer_delegates_to_tx_core(self): from world_sync import pbl_runtime_tx, pbl_runtime_tx_sqlor assert callable(pbl_runtime_tx_sqlor.write_event_with_state) assert pbl_runtime_tx_sqlor._write_event_with_state is \ pbl_runtime_tx.write_event_with_state class TestNoForbiddenLogic(object): """验收标准「不引入广播逻辑、不引入轮询逻辑」的机械守卫。""" FORBIDDEN = ("broadcast", "publish_to", "subscribe", "poll(", "polling", "while True:", "time.sleep(", "websocket", "event_source") TARGETS = ("pbl_runtime_tx.py", "pbl_runtime_tx_sqlor.py", "pbl_runtime_sql.py", "pbl_runtime_errors.py", "pbl_runtime_tx_env.py") def test_no_broadcast_or_polling_in_m11b2_modules(self): root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) pkg = os.path.join(root, "world_sync") for name in self.TARGETS: with open(os.path.join(pkg, name), "r", encoding="utf-8") as fh: lines = fh.readlines() for idx, ln in enumerate(lines, 1): low = ln.lower() for token in self.FORBIDDEN: if token in low and not low.lstrip().startswith("#"): pytest.fail("%s:%d contains forbidden %r" % (name, idx, token)) def test_no_network_imports(self): root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) pkg = os.path.join(root, "world_sync") for name in self.TARGETS: with open(os.path.join(pkg, name), "r", encoding="utf-8") as fh: text = fh.read() for mod in ("import asyncio", "import socket", "import requests", "import aiohttp"): assert mod not in text, "%s %s" % (name, mod)