本文系《pytest源码剖析》系列内容
34. capturemanager
插件路径:_pytest.capture.CaptureManager
本插件是capture的子插件,建议同步参阅《pytest的内置插件盘点11. capture》
实现的 hook
调用的 hook
无
定义的 fixture
无
插件功能
在用例收集阶段,进行全局捕获
在用例执行阶段,进行全局捕获和 fixture 捕获
提供了一个暂停捕获的方法,供 fixture 使用
代码片段
def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None: self._capture_fixture = capture_fixture
def unset_fixture(self) -> None: self._capture_fixture = None @contextlib.contextmanager def item_capture(self, when: str, item: Item) -> Generator[None, None, None]: self.resume_global_capture() self.activate_fixture() try: yield finally: self.deactivate_fixture() self.suspend_global_capture(in_=False)
out, err = self.read_global_capture() item.add_report_section(when, "stdout", out) item.add_report_section(when, "stderr", err)
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: if method == "fd": return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) elif method == "sys": return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2)) elif method == "no": return MultiCapture(in_=None, out=None, err=None) elif method == "tee-sys": return MultiCapture( in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True) ) raise ValueError(f"unknown capturing method: {method!r}")
capturemanager 允许动态设置和控制 fixture 捕获器
执行用例时先激活全局捕获,再激活 fixture 捕获,捕获结果会附加到 item 中
只有no和tee-sys没有对输入进行捕获,能够进行正常输入
简评
capture 插件在加载 conftest.py 之前,获取capture参数的值,并据此实例化和注册了 capturemanager 插件
...
capturemanager 插件对标准输入 / 输出的捕获,分为两种:
第一种是全局捕获,根据capture参数创建捕获器,
捕获器会将目标 IO 对象 mock 掉,从而得到输出的内容
第二种是 fixture 捕获,由capsys、capfd等几个 fixture 控制
fixture 被用例请求后,会创建捕获器并传递到 capturemanager 插件中
因为插件只接收一个 fixture 捕获器,所以capsys、capfd不能同时使用
...
在用例执行阶段 ,capturemanager 先激活全局捕获,再激活 fixture 捕获
假设在用例中执行了print语句
def test_print_with(capsys): print('123') assert False
那么 fixture 捕获器将得到输出内容:123\n
至于全局捕获器不会得到任何内容
但是实际结果是
_______________________________ test_print_with _______________________________
capfd = <_pytest.capture.CaptureFixture object at 0x0000010AF3AAB770>
def test_print_with(capfd): print('123')> assert FalseE assert False
test_io.py:10: AssertionError---------------------------- Captured stdout call -----------------------------123
这里的Captured stdout call很明显是全局捕获器提供给 item 的
这就很奇怪了:
fixture 捕获器拦截输出内容之后,全局捕获器到底还有没有得到输出内容?
...
修改插件内容,加入如下代码:
从 debug 结果来看,使用capfd时,输出内容存储在 fixture 捕获器中
当用例不使用任何 fixture 时,输出内容存储在全局捕获器中
...
于是逐行调试,确定了每一行代码的作用
@contextlib.contextmanagerdef item_capture(self, when: str, item: Item) -> Generator[None, None, None]: self.resume_global_capture() # 开始全局捕获 self.activate_fixture() # 开始fixture获取 try: yield # 执行用例 finally: self.deactivate_fixture() # 将fixture捕获到的内容,追加到全局捕获中 self.suspend_global_capture(in_=False) # 停止全局捕获
out, err = self.read_global_capture() # 读取全局捕获内容 item.add_report_section(when, "stdout", out) # stdout附加到用例中 item.add_report_section(when, "stderr", err) # stderr附加到用例中
进一步跟踪发现,
class CaptureFixture def close(self) -> None: if self._capture is not None: out, err = self._capture.pop_outerr_to_orig() # 全局捕获结果发送变化
这个 pop_outerr_to_orig 方法,获取当前捕获内容 并重写一份到原始 IO 中
这下就彻底搞清楚了:
fixture 捕获是货真价实的拦截、捕获
全局捕获也是货真价实的拦截、捕获
fixture 捕获成功之后,会重写一份
重写的内容被全局捕获器捕获,附加到用例中,显示在报告里
之所以这么做,很可能是为了避免 fixtures 捕获器把重要的信息屏蔽,导致用例失败时无法准确排查
可以说考虑得非常贴心,全面了,不愧是维护了十几年的老牌开源框架
领取专属 10元无门槛券
私享最新 技术干货