pytest框架插件源码_关于钩子方法调用部分的简单理解(pytest_runtest_makereport)
前言:
因为想不明白写的pytest_runtest_makereport里的yield是怎么把结果传出来的?pytest是怎么调用的我们自己写的pytest_runtest_makereport方法?一不小心给自己开了新坑……熬了两个晚上啃了源码,终于对整个流程稍微有点思路……
P.S. 参考1中的教程非常详细的解释了pluggy源码,对pytest插件执行流程的理解非常有帮助,建议深读
因为是边单步执行源码,边查资料理解,边写完这篇博客,所有前面部分会有点乱,有空了再整理吧,尽可能把我理解的东西写出来。
首先,贴源码
我在conftest.py里写的pytest_runtest_makereport方法代码如下
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
print("ininin")
out = yield
res = out.get_result()
print(res)
if res.when == "call":
logging.info(f"item:{item}")
logging.info(f"异常:{call.excinfo}")
logging.info(f"故障表示:{res.longrepr}")
logging.info(f"测试结果:{res.outcome}")
logging.info(f"用例耗时:{res.duration}")
logging.info("**************************************")
经过打断点,知道pytest_runtest_makereport是由这方法调用的
# site-packages\pluggy\callers.py
def _multicall(hook_impls, caller_kwargs, firstresult=False):
"""Execute a call into multiple python functions/methods and return the
result(s).
``caller_kwargs`` comes from _HookCaller.__call__().
"""
__tracebackhide__ = True
results = []
excinfo = None
try: # run impl and wrapper setup functions in a loop
teardowns = []
try:
for hook_impl in reversed(hook_impls):
try:
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
except KeyError:
for argname in hook_impl.argnames:
if argname not in caller_kwargs:
raise HookCallError(
"hook call must provide argument %r" % (argname,)
)
if hook_impl.hookwrapper:
try:
gen = hook_impl.function(*args)
next(gen) # first yield
teardowns.append(gen)
except StopIteration:
_raise_wrapfail(gen, "did not yield")
else:
res = hook_impl.function(*args)
if res is not None:
results.append(res)
if firstresult: # halt further impl calls
break
except BaseException:
excinfo = sys.exc_info()
finally:
if firstresult: # first result hooks return a single value
outcome = _Result(results[0] if results else None, excinfo)
else:
outcome = _Result(results, excinfo)
# run all wrapper post-yield blocks
for gen in reversed(teardowns):
try:
gen.send(outcome)
_raise_wrapfail(gen, "has second yield")
except StopIteration:
pass
return outcome.get_result()
其中根据大佬的解析可知:
- 插件会先注册使得存在这个接口类
- 调用这个接口会跳到实现函数,也就是我们写的pytest_runtest_makereport
具体来一步步看
一、 实现函数使用装饰器
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
- 根据pycharm跳转hookimpl的来源,可知
hookimpl = HookimplMarker("pytest")
hookspec = HookspecMarker("pytest")
hookimpl 是HookimplMarker()的实例化
- HookimplMarker()类
# site-packages\pluggy\hooks.py
class HookimplMarker(object):
""" Decorator helper class for marking functions as hook implementations.
You can instantiate with a ``project_name`` to get a decorator.
Calling :py:meth:`.PluginManager.register` later will discover all marked functions
if the :py:class:`.PluginManager` uses the same project_name.
"""
def __init__(self, project_name):
self.project_name = project_name
def __call__(
self,
function=None,
hookwrapper=False,
optionalhook=False,
tryfirst=False,
trylast=False,
):
def setattr_hookimpl_opts(func):
setattr(
func,
self.project_name + "_impl",
dict(
hookwrapper=hookwrapper,
optionalhook=optionalhook,
tryfirst=tryfirst,
trylast=trylast,
),
)
return func
if function is None:
return setattr_hookimpl_opts
else:
return setattr_hookimpl_opts(function)
# 其中还有
可知,HookimplMarker类存在__call__魔法方法,也就是类在实例化之后,可以想普通函数一样进行调用。
hookimpl = HookimplMarker("pytest")
这一步实例化,走__init__魔法方法,即hookimpl 拥有了变量project_name,值为"pytest"回到@pytest.hookimpl(hookwrapper=True, tryfirst=True)
也就是说hookimpl这里就进到了__call__里面
传了两个参数hookwrapper、tryfirst,其他为默认值- setattr(object, name, value)
给object设置属性name的属性值value(不存在name属性就新增)
- setattr(object, name, value)
这段代码简单来说就是给被装饰的函数添加属性值return setattr_hookimpl_opts(function)
属性名为self.project_name + "_impl"
,也就是"pytest_impl"
属性值为一个字典,包括hookwrapper、optionalhook、tryfirst、trylast这几个key
最后返回被装饰的函数return func
这个时候pytest_runtest_makereport函数就有了pytest_impl属性值
二、 接下来就是使用PluginManager类创建接口类,并加到钩子定义中,注册实现函数,这部分先略过
简单来说,经过这步这个函数就可以作为钩子调用了
接口方法拥有project_name+"_spec"(即"pytest_spec")属性,属性值为一个字典,包括firstresult,historic,warn_on_impl这3个key
hookwrapper=Ture
则把实现函数放到了_wrappers列表中实例化HookImpl对象,存放实现函数的信息
给self.hook 添加了名为实现方法的函数名的属性,属性值为
_HookCaller(name, self._hookexec)
_HookCaller(name, self._hookexec)这里依然是调了_HookCaller类的__call__方法,返回了
self._hookexec(self, self.get_hookimpls(), kwargs)
self.get_hookimpls() 返回的是
self._nonwrappers + self._wrappers
,也就是实现函数列表
三、跳转到实现函数
应该是触发钩子接口后,跳转到_multicall方法,接下来就是进入实现函数的控制执行了
- 首先是循环该接口的实现函数
也就是所有注册的pytest_runtest_makereport方法
def _multicall(hook_impls, caller_kwargs, firstresult=False):
"""Execute a call into multiple python functions/methods and return the
result(s).
``caller_kwargs`` comes from _HookCaller.__call__().
"""
__tracebackhide__ = True
results = []
excinfo = None
try: # run impl and wrapper setup functions in a loop
teardowns = []
try:
for hook_impl in reversed(hook_impls):
……
由代码可知,for hook_impl in reversed(hook_impls)
,hook_impls里存放的是所有的实现函数,reversed倒序返回列表(先注册的实现函数会存在hook_impls[0],也就是说这里会先执行后注册的实现函数)
pytest_runtest_makereport共有4个插件,也就是有4个实现函数
2. 把caller_kwargs[argname]存到args
也就是(iten,call),为了传参给实现函数
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
3. 跳转到实现函数
if hook_impl.hookwrapper: # 取实现函数的hookwrapper属性进行判断,如果hookwrapper为Ture,则说明实现函数为生成器
try:
gen = hook_impl.function(*args) # gen为pytest_runtest_makereport生成器
next(gen) # first yield # 走到这步的时候跳转到实现函数
teardowns.append(gen) # 执行到实现函数的yeild回到这里,把生成器放入teardowns
except StopIteration:
_raise_wrapfail(gen, "did not yield")
执行完这一步,又继续循环reversed(hook_impls)
跳转到pytest_runtest_makereport的实现函数(这部分应该是pytest原有的实现函数)
代码如下
# _pytest.skipping.py
@hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
outcome = yield
rep = outcome.get_result()
xfailed = item._store.get(xfailed_key, None)
# unittest special case, see setting of unexpectedsuccess_key
if unexpectedsuccess_key in item._store and rep.when == "call":
reason = item._store[unexpectedsuccess_key]
if reason:
rep.longrepr = f"Unexpected success: {reason}"
else:
rep.longrepr = "Unexpected success"
rep.outcome = "failed"
elif item.config.option.runxfail:
pass # don't interfere
elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception):
assert call.excinfo.value.msg is not None
rep.wasxfail = "reason: " + call.excinfo.value.msg
rep.outcome = "skipped"
elif not rep.skipped and xfailed:
if call.excinfo:
raises = xfailed.raises
if raises is not None and not isinstance(call.excinfo.value, raises):
rep.outcome = "failed"
else:
rep.outcome = "skipped"
rep.wasxfail = xfailed.reason
elif call.when == "call":
if xfailed.strict:
rep.outcome = "failed"
rep.longrepr = "[XPASS(strict)] " + xfailed.reason
else:
rep.outcome = "passed"
rep.wasxfail = xfailed.reason
if (
item._store.get(skipped_by_mark_key, True)
and rep.skipped
and type(rep.longrepr) is tuple
):
# Skipped by mark.skipif; change the location of the failure
# to point to the item definition, otherwise it will display
# the location of where the skip exception was raised within pytest.
_, _, reason = rep.longrepr
filename, line = item.reportinfo()[:2]
assert line is not None
rep.longrepr = str(filename), line + 1, reason
之后循环实现函数_pytest.unittest.py、runner.py的实现函数,就不重复贴代码了
进入实现函数都会执行一次各个实现函数的代码
- 接下来会跑pytest_runtest_logreport、pytest_report_teststatus、pytest_runtest_protocol、pytest_runtest_logstart、pytest_runtest_setup、pytest_fixture_setup等接口的实现函数(可能需要调用这些函数返回什么信息吧)
这块的流程不太清楚,感觉可能在_multicall的上一层应该还有一个控制函数,触发了哪些接口,再调_multicall跑这些接口的实现函数?也有可能debug调试的时候,我点太快跑飞了……
- 跑完实现函数后,进入finally部分,赋值outcome
finally:
if firstresult: # first result hooks return a single value
outcome = _Result(results[0] if results else None, excinfo)
else:
outcome = _Result(results, excinfo)
- 跑完实现函数之后,最后会把之前存在teardown里的生成器(为生成器的实现函数)跑完,把outcome的值传给生成器
# run all wrapper post-yield blocks
for gen in reversed(teardowns):
try:
gen.send(outcome)
_raise_wrapfail(gen, "has second yield")
except StopIteration:
pass
`gen.send(outcome)` 把outcome的值传给生成器,生成器会从上一次yeild的地方往下跑
也就是回到的conftest.py的pytest_runtest_makereport的实现函数里的
outcome = yield
这行
def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
outcome = yield # 这里
rep = outcome.get_result()
新建变量outcome接收了传过来的outcome
这里涉及到生成器的知识
- 调用生成器执行到yield,返回到调用函数,生成器的变量状态保留
- 使用send()方法,可把调用函数的值传给生成器
- 这里还有一个小知识点,生成器第一次调用的时候不可以使用send()方法传值,会报错
TypeError: can't send non-None value to a just-started generator
简单写个生成器调用,流程和pytest里执行实现函数是一样的,单步执行跑一下代码就理解了
def fun2():
print("fun2")
out = yield
print("fun22")
print(f"out:{out}")
def fun3():
print("fun3")
f = fun2()
next(f) # 调用生成器fun(2), 执行到fun2的 yield后返回
f.send("00") # 再第二次调用生成器fun2,并传个值"00",因为上次fun2执行到yield,这次调用从yeild开始执行,所以值传给了fun2的out变量
print("fun33")
if __name__ == '__main__':
fun3()
# --------输出---------
# fun3
# fun2
# fun22
# out:00
# 报错 StopIteration,执行这里迭代器没有下一个值了所以报错,之后也没法print("fun33")
四、 之后执行pytest_runtest_makereport方法的代码就没什么可说的,自己写的逻辑很简单
最后跳出来到了_pytest/runner.py的call_and_report方法
report: TestReport = hook.pytest_runtest_makereport(item=item, call=call)
return report
再跳到runtestprotocol方法
总结:
一、所谓的钩子函数hook
有一个方法A,还有另外一个方法B,执行到方法A的时候跳转到方法B,这就是实现了hook的作用。
如何能把方法A和方法B关联起来,就用到一个起到注册功能的方法,通过这个方法实现两个方法的关联。
def fun1():
print("fun1")
return "out"
class TestHook:
def __init__(self):
self.hook_fun = None
def register_fun2_hook(self,fun):
self.hook_fun = fun
def fun2(self):
print("这里是fun2")
if self.hook_fun:
self.hook_fun()
else:
print("no hook")
if __name__ == '__main__':
xxx = TestHook()
xxx.register_fun2_hook(fun1)
xxx.hook_fun()
print('*********')
xxx.fun2()
# -----输出-----
# fun1
# *********
# 这里是fun2
# fun1
实例化TestHook这个类,hook_fun为None
调用register_fun2_hook方法,注册self.hook_fun,使得self.hook_fun与传入的参数fun进行关联,这个fun就是我们另外自定义的方法B,self.hook_fun就是钩子函数
执行xxx.fun2(),就会去执行fun1
说回pytest,self.hook_fun 就是 runner.py 定义的接口函数 pytest_runtest_makereport ,fun1 就是我们在 conftest.py 写的实现函数pytest_runtest_makereport
二、pytest里的hook实现
定义接口类,在接口类添加接口函数 pytest_runtest_makereport
定义插件类,插件里添加实现函数 pytest_runtest_makereport
实例化插件管理对象pm
调用pm.add_hookspecs(),把创建的接口 pytest_runtest_makereport添加到钩子定义中
注册实现函数 pytest_runtest_makereport
hook.pytest_runtest_makereport 调用钩子函数
通过cller类的_multicall方法控制实现执行接口的所有实现函数
参考1:https://blog.csdn.net/redrose2100/article/details/121277958
参考2:https://docs.pytest.org/en/latest/reference/reference.html?highlight=pytest_runtest_makereport#std-hook-pytest_runtest_makereport
pytest框架插件源码_关于钩子方法调用部分的简单理解(pytest_runtest_makereport)的更多相关文章
- 基于tomcat插件的maven多模块工程热部署(附插件源码)
内容属原创,转载请注明出处 写在前面的话 最近一直比较纠结,归根结底在于工程的模块化拆分.以前也干过这事,但是一直对以前的结果不满意,这会重操旧业,希望搞出个自己满意的结果. 之前有什么不满意的呢? ...
- 如何查看JDK以及JAVA框架的源码
如何查看JDK以及JAVA框架的源码 设置步骤如下: 1.点 “window”-> "Preferences" -> "Java" -> &q ...
- 如何查看google chrome 插件源码
常用浏览器google chrome 有很多优秀的插件,寂寞的时候想看看人家是怎么实现的,说是快那就动手吧 插件代码位置 本人mac笔记本,chrome 插件位置如下 $ cd /Users/vin ...
- 高性能网络I/O框架-netmap源码分析
from:http://blog.chinaunix.net/uid-23629988-id-3594118.html 博主这篇文章写的很好 感觉很有借签意义 值得阅读 高性能网络I/O框架-netm ...
- 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(2)-easyui构建前端页面框架[附源码]
原文:构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(2)-easyui构建前端页面框架[附源码] 开始,我们有了一系列的解决方案,我们将动手搭建新系统吧. 用 ...
- robotlegs2.0框架实例源码带注释
robotlegs2.0框架实例源码带注释 Robotlegs2的Starling扩展 有个老外写了robotleges2的starling扩展,地址是 https://github.com/brea ...
- 【安卓网络请求开源框架Volley源码解析系列】定制自己的Request请求及Volley框架源码剖析
通过前面的学习我们已经掌握了Volley的基本用法,没看过的建议大家先去阅读我的博文[安卓网络请求开源框架Volley源码解析系列]初识Volley及其基本用法.如StringRequest用来请求一 ...
- Ocelot简易教程(七)之配置文件数据库存储插件源码解析
作者:依乐祝 原文地址:https://www.cnblogs.com/yilezhu/p/9852711.html 上篇文章给大家分享了如何集成我写的一个Ocelot扩展插件把Ocelot的配置存储 ...
- Python 基于python实现的http接口自动化测试框架(含源码)
基于python实现的http+json协议接口自动化测试框架(含源码) by:授客 QQ:1033553122 欢迎加入软件性能测试交流 QQ群:7156436 由于篇幅问题,采用百度网 ...
- 【Struts2】如何查看Struts2框架的源码
学习三大框架时难免遇到不太理解的地方需要去研究框架源码,这里总结一下查看struts2源码的两种方式. 1.直接解压struts2.X.X-all.zip,在的到的解压文件中看到如下目录: 打开图中蓝 ...
随机推荐
- 20 张图带你全面了解 HTTPS 协议,再也不怕面试问到了!
本文详细介绍了 HTTPS 相较于 HTTP 更安全的原因,包括对称加密.非对称加密.完整性摘要.数字证书以及 SSL/TLS 握手等内容,图文并茂.理论与实战结合.建议收藏! 1. 不安全的 HTT ...
- ffmpeg第8篇:使用ffprobe采集文件信息
1. 前言 ffprobe是ffmpeg的其中一个模块,主要用于查看文件信息,咱们知道一个MP4文件其实不仅仅包含了音视频数据,还有如元数据等其它信息,但是实际上咱们关心的往往是音视频数据部分,今天来 ...
- 如何用 30s 给面试官讲清楚什么是 Token?
引言 前文介绍了 Session-Cookie 的认证过程,简单回顾下基本步骤: 客户端(浏览器)向服务器发送用户名和密码 服务器验证通过后,创建 Session 对象,在 Session 中保存该用 ...
- Linux c 获取U盘挂载路径
思路: 1.执行df -h 找到 带mnt的行.将结果存入一个文件中. system("df -h |grep mnt >./extendevinfo.txt"); 也可以直 ...
- 一文详解如何在基于webpack5的react项目中使用svg
本文主要讨论基于webpack5+TypeScript的React项目(cra.craco底层本质都是使用webpack,所以同理)在2023年的今天是如何在项目中使用svg资源的. 首先,假定您已经 ...
- dapr本地托管的服务调用体验与Java SDK的Spring Boot整合
1 简介 之前在文章<dapr入门与本地托管模式尝试>中介绍了dapr和本地托管,本文我们来介绍如果在代码中使用dapr的服务调用功能,并把它整合到Spring Boot中. Dapr服务 ...
- UML 图
类的表示(Class) 第一层:显示类的名称,如果是抽象类,则就用斜体显示. 第二层:是类的特性,通常就是字段和属性. 第三层:是类的操作,通常是方法或行为(前面加号(+)表示public:减号(-) ...
- Deep Learning-Based Monocular Depth Estimation Methods-A State-of-the-Art Review
注:刚入门depth estimation,这也是以后的主要研究方向,欢迎同一个方向的加入QQ群(602708168)交流. 1. 论文简介 论文题目:Deep Learning-Based Mono ...
- socket模块实现网络编程及struct模块解决黏包问题
目录 一.socket模块 1.简介 2.基于文件类型的套接字家族 3.基于网络类型的套接字家族 二.socket代码简介 三.socket代码优化 1.聊天内容自定义 2.让聊天循环起来 3.用户输 ...
- python学习day 02
昨日内容回顾 typora软件 1.作为一款逐年火爆的文本编辑器,深受IT行业的喜爱. 2.下载与安装: windows用群里发的软件 macOS下载地址:https://mac.qdrayst.co ...