"""五个接缝的契约套件,随包发布,给下游当准入标准用。 每个接缝一个基类。这套用例不针对任何具体实现,写的是「不管你怎么实现,都必须满足这些 行为」——所以它自己不造实现,只声明「你得提供什么」。写完自己的存储或适配器之后,在自己的 测试文件里继承对应的基类、覆盖那几个必需 fixture,跑一遍全绿就算合格。 # tests/test_my_store.py import pytest from polyloop.testing import RunStoreContract from myproject.storage import MyPostgresStore class TestMyPostgresStore(RunStoreContract): @pytest.fixture def store(self, pg_pool): return MyPostgresStore(pg_pool) 子类的类名要以 `Test` 开头,pytest 才收集它。基类自己叫 `...Contract` 正是为了不被收集: pytest 按 `Test` 前缀匹配测试类,一个叫 `RunStoreContract` 的基类不会被当成测试类跑一遍, 于是它那些 `raise NotImplementedError` 的默认 fixture 也就不会失败。 **装它**:`pip install "polyloop[testing]"`。 **必需的 fixture 在基类里都有一个默认实现,函数体是 `raise NotImplementedError`**,报错消息 写着该覆盖什么、该返回什么。忘了覆盖时看到的是这条消息,而不是 pytest 那句 `fixture 'store' not found` 加一整屏 available fixtures 列表——后者指向的是 site-packages 里 的库文件,第一反应会是库坏了。 **样本输入由实现方提供**,见 `DecisionParserContract.reply_samples`、 `ActionExecutorContract.action_samples` 与 `ModelClientContract.failing_call` 的 docstring。 套件不认识任何一家的动作语言,也不知道一家实现要怎样才会失败:拿一家的代码围栏去喂另一家的 JSON 解析器,后者正确地返回「无效决策」,而写死输入的套件会把这个正确行为判成失败。 **实现之间的能力差异走运行期跳过,不走失败。** 取消那两条就是这么处理的——一个在单个事件 循环 tick 之内就返回的实现根本没有机会吞掉取消,那条契约对它无从谈起,跳过的理由字符串会 说清楚这一点。看到这种跳过不必去改自己的实现。 **async 用例的事件循环归下游管。** 套件里的用例照常写成 `async def`,套件不做任何事件循环 安排。所以要么把 `asyncio_mode` 设成 `"auto"`(`pyproject.toml` 的 `[tool.pytest.ini_options]` 里),要么自己给子类打上对应的标记。没配对的失败是响亮的: pytest 会明说 async def 函数不被原生支持,那条信息直接指向解法。 库不替下游安排循环,是因为下游的 fixture 很可能是异步的——一个数据库存储实现的连接池就是。 那个 fixture 在下游的循环里创建,库自己开的循环是另一个,跨循环使用 asyncio 对象会炸,而且 炸得很难查。 **`PYTEST_DISABLE_PLUGIN_AUTOLOAD=1` 的环境要多写一行。** 本包声明了一个 pytest 插件入口, 它唯一的作用是让 pytest 重写这些模块里的 `assert`,失败时打印出等号两边的实际值。那个环境 变量一设上,入口就不加载了,契约失败会退化成光秃秃的 `AssertionError`。这时在自己的 `conftest.py` 顶上补一行: pytest.register_assert_rewrite("polyloop.testing") **套件里一个自定义 marker 都不用。** 开了 `--strict-markers` 而没注册那个 marker 的话,炸掉 的是整个测试文件的收集,不是一条失败,而报错指向的同样是库的文件。要给这些用例分组就在自己 的子类文件上打标记,那个文件和那个 marker 都在你自己的仓库里。 **这里的每个名字都是公共承诺**:基类名、基类上的 fixture 名、每一条用例的方法名。下游的子类 按它们写,改名的代价和改公共类型的字段一样。用例方法名尤其要紧——下游要豁免某一条时按名字 重绑它,改名之后那个豁免会悄悄失效,跑出来照样全绿。 设计与取舍见 `research-wiki/design/0014-contract-suite-distribution.md`。 """ from polyloop.testing._action_executor import ActionExecutorContract from polyloop.testing._decision_parser import DecisionParserContract from polyloop.testing._event_sink import EventSinkContract from polyloop.testing._model_client import ModelClientContract from polyloop.testing._records import RecordFactory from polyloop.testing._run_store import RunStoreContract __all__ = [ "ActionExecutorContract", "DecisionParserContract", "EventSinkContract", "ModelClientContract", "RecordFactory", "RunStoreContract", ]