前言:

因为想不明白写的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()

其中根据大佬的解析可知:

  1. 插件会先注册使得存在这个接口类
  2. 调用这个接口会跳到实现函数,也就是我们写的pytest_runtest_makereport

具体来一步步看

一、 实现函数使用装饰器

@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
  1. 根据pycharm跳转hookimpl的来源,可知
hookimpl = HookimplMarker("pytest")
hookspec = HookspecMarker("pytest")

hookimpl 是HookimplMarker()的实例化

  1. 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__魔法方法,也就是类在实例化之后,可以想普通函数一样进行调用。

  1. hookimpl = HookimplMarker("pytest")这一步实例化,走__init__魔法方法,即hookimpl 拥有了变量project_name,值为"pytest"

  2. 回到@pytest.hookimpl(hookwrapper=True, tryfirst=True)

    也就是说hookimpl这里就进到了__call__里面

    传了两个参数hookwrapper、tryfirst,其他为默认值

    • setattr(object, name, value)

      给object设置属性name的属性值value(不存在name属性就新增)

这段代码简单来说就是给被装饰的函数添加属性值return setattr_hookimpl_opts(function)

属性名为self.project_name + "_impl",也就是"pytest_impl"

属性值为一个字典,包括hookwrapper、optionalhook、tryfirst、trylast这几个key

最后返回被装饰的函数return func

这个时候pytest_runtest_makereport函数就有了pytest_impl属性值

二、 接下来就是使用PluginManager类创建接口类,并加到钩子定义中,注册实现函数,这部分先略过

简单来说,经过这步这个函数就可以作为钩子调用了

  1. 接口方法拥有project_name+"_spec"(即"pytest_spec")属性,属性值为一个字典,包括firstresult,historic,warn_on_impl这3个key

  2. hookwrapper=Ture则把实现函数放到了_wrappers列表中

  3. 实例化HookImpl对象,存放实现函数的信息

  4. 给self.hook 添加了名为实现方法的函数名的属性,属性值为_HookCaller(name, self._hookexec)

  5. _HookCaller(name, self._hookexec)这里依然是调了_HookCaller类的__call__方法,返回了self._hookexec(self, self.get_hookimpls(), kwargs)

  6. self.get_hookimpls() 返回的是self._nonwrappers + self._wrappers,也就是实现函数列表

三、跳转到实现函数

应该是触发钩子接口后,跳转到_multicall方法,接下来就是进入实现函数的控制执行了

  1. 首先是循环该接口的实现函数

    也就是所有注册的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的实现函数,就不重复贴代码了

进入实现函数都会执行一次各个实现函数的代码

  1. 接下来会跑pytest_runtest_logreport、pytest_report_teststatus、pytest_runtest_protocol、pytest_runtest_logstart、pytest_runtest_setup、pytest_fixture_setup等接口的实现函数(可能需要调用这些函数返回什么信息吧)

这块的流程不太清楚,感觉可能在_multicall的上一层应该还有一个控制函数,触发了哪些接口,再调_multicall跑这些接口的实现函数?也有可能debug调试的时候,我点太快跑飞了……

  1. 跑完实现函数后,进入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)
  1. 跑完实现函数之后,最后会把之前存在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
  1. 实例化TestHook这个类,hook_fun为None

  2. 调用register_fun2_hook方法,注册self.hook_fun,使得self.hook_fun与传入的参数fun进行关联,这个fun就是我们另外自定义的方法B,self.hook_fun就是钩子函数

  3. 执行xxx.fun2(),就会去执行fun1
  4. 说回pytest,self.hook_fun 就是 runner.py 定义的接口函数 pytest_runtest_makereport ,fun1 就是我们在 conftest.py 写的实现函数pytest_runtest_makereport

二、pytest里的hook实现

  1. 定义接口类,在接口类添加接口函数 pytest_runtest_makereport

  2. 定义插件类,插件里添加实现函数 pytest_runtest_makereport

  3. 实例化插件管理对象pm

  4. 调用pm.add_hookspecs(),把创建的接口 pytest_runtest_makereport添加到钩子定义中

  5. 注册实现函数 pytest_runtest_makereport

  6. hook.pytest_runtest_makereport 调用钩子函数

  7. 通过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)的更多相关文章

  1. 基于tomcat插件的maven多模块工程热部署(附插件源码)

    内容属原创,转载请注明出处 写在前面的话 最近一直比较纠结,归根结底在于工程的模块化拆分.以前也干过这事,但是一直对以前的结果不满意,这会重操旧业,希望搞出个自己满意的结果. 之前有什么不满意的呢? ...

  2. 如何查看JDK以及JAVA框架的源码

    如何查看JDK以及JAVA框架的源码 设置步骤如下: 1.点 “window”-> "Preferences" -> "Java" -> &q ...

  3. 如何查看google chrome 插件源码

    常用浏览器google chrome 有很多优秀的插件,寂寞的时候想看看人家是怎么实现的,说是快那就动手吧 插件代码位置 本人mac笔记本,chrome 插件位置如下 $ cd  /Users/vin ...

  4. 高性能网络I/O框架-netmap源码分析

    from:http://blog.chinaunix.net/uid-23629988-id-3594118.html 博主这篇文章写的很好 感觉很有借签意义 值得阅读 高性能网络I/O框架-netm ...

  5. 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(2)-easyui构建前端页面框架[附源码]

    原文:构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(2)-easyui构建前端页面框架[附源码] 开始,我们有了一系列的解决方案,我们将动手搭建新系统吧. 用 ...

  6. robotlegs2.0框架实例源码带注释

    robotlegs2.0框架实例源码带注释 Robotlegs2的Starling扩展 有个老外写了robotleges2的starling扩展,地址是 https://github.com/brea ...

  7. 【安卓网络请求开源框架Volley源码解析系列】定制自己的Request请求及Volley框架源码剖析

    通过前面的学习我们已经掌握了Volley的基本用法,没看过的建议大家先去阅读我的博文[安卓网络请求开源框架Volley源码解析系列]初识Volley及其基本用法.如StringRequest用来请求一 ...

  8. Ocelot简易教程(七)之配置文件数据库存储插件源码解析

    作者:依乐祝 原文地址:https://www.cnblogs.com/yilezhu/p/9852711.html 上篇文章给大家分享了如何集成我写的一个Ocelot扩展插件把Ocelot的配置存储 ...

  9. Python 基于python实现的http接口自动化测试框架(含源码)

    基于python实现的http+json协议接口自动化测试框架(含源码) by:授客 QQ:1033553122      欢迎加入软件性能测试交流 QQ群:7156436  由于篇幅问题,采用百度网 ...

  10. 【Struts2】如何查看Struts2框架的源码

    学习三大框架时难免遇到不太理解的地方需要去研究框架源码,这里总结一下查看struts2源码的两种方式. 1.直接解压struts2.X.X-all.zip,在的到的解压文件中看到如下目录: 打开图中蓝 ...

随机推荐

  1. 【Vue3.0】关于 script setup 语法糖的用法

    script setup - 简介 先来看一看官网关于 <script setup> 的介绍: 要彻底的了解 setup 语法糖,你必须先明确 setup() 这个 组合式API 官网中对 ...

  2. Less常用功能使用

    Less 是一门 CSS 预处理语言,它扩充了 CSS 语言,增加了诸如变量.混合(mixin).函数等功能,让 CSS 更易维护.方便制作主题.扩充.Less 可以运行在 Node 或浏览器端. L ...

  3. 解决使用linux部署nodejs服务测试代码返回中文是乱码

    今天写了个简单的node.js文件 代码如下 var http = require('http'); http.createServer(function (request, response) { ...

  4. 使用ng-zorro图标库出现the icon redo-o does not exist or is not registered.

    出现这种情况一般是因为没导入你要的图标 可以在项目目录找到这个文件  src\style-icons-auto.ts 然后打开,导入导出里加上你要导入的图标....  就可以了 如果你不知道要怎么加 ...

  5. order by 语句怎么优化?

    说明 当前演示的数据库版本5.7 一.一个简单使用示例 先创建一张订单表 CREATE TABLE `order_info` ( `id` int NOT NULL AUTO_INCREMENT CO ...

  6. 【分析笔记】Linux tasklet 机制的理解

    Tasklet 介绍 Linux 内核提供的四种中断下半部中 softirq(软中断).tasklet(小任务).workqueue(工作队列) .request thread(中断线程)中的其中一种 ...

  7. c++ 递推算法

    各位大佬不妨先点个赞再看文章! 递推法是一种重要的数学方法,在数学的各个领域中都有广泛的运用,也是计算机用于数值计算的一个重要算法.这种算法特点是:一个问题的求解需一系列的计算,在已知条件和所求问题之 ...

  8. 11月30日内容总结——前端简介、http协议概念、html协议概念及基础知识和部分标签的讲解

    目录 一.前端与后端的概念 什么是前端开发? 什么是后端? 学习前端的目的 前端三剑客 二.前端前戏 三.HTTP协议 1.四大特性 2.报文格式 3.响应状态码 四.HTML概览 1.HTML简介 ...

  9. Stochastic Methods in Finance (1)

    Bootcamp Topics related to measure theory. 略去,详见测度论专栏中的文章 Expectations 令 \(X\) 为 \((\Omega, \mathcal ...

  10. 【亲妈教学】配置Gzip压缩,含前后端步骤

    前言 在使用 vite 进行项目打包时,默认已经帮我们做了一些优化工作,比如代码的压缩,分包等等. 除此之外,我们还有一些可选的优化策略,比如使用 CDN ,开启 Gzip 压缩等.本文会介绍在 vi ...