前言:

因为想不明白写的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. day07-Spring管理Bean-IOC-05

    Spring管理Bean-IOC-05 3.基于注解配置bean 3.3自动装配 基本说明: 基于注解配置bean,也可以实现自动装配,使用的注解是:@AutoWired或者@Resource @Au ...

  2. Springboot+Dplayer+RabbitMQ实现视频弹幕延时入库

    编写之初,在网上找了很多关于springboot整合dplayer实现弹幕的方式,发现案例很少,然后自己就着手写一个小项目,分享给大家~ 注:Dplayer版本:v1.22.2 流程:前端自定义弹幕发 ...

  3. Keepalived高可用集群部署

    KeepAlived 目录 KeepAlived KeepAlived安装 KeepAlived部署 准备工作 主备模式 节点配置 验证 正常状态 故障 故障恢复 1+N(一主多备)模式 节点配置 验 ...

  4. Google Cloud Platform | 使用 Terraform 的分层防火墙策略自动化

    [本文由Cloud Ace整理发布,更多内容请访问Cloud Ace 官网] 防火墙规则是 Google Cloud 中网络安全的重要组成部分.Google Cloud 中的防火墙大致可分为两种类型: ...

  5. java入门与进阶P-4.7

    最大公约数 首先做这个题需要先复习几组概念: 如果数a能被数b整除,a就叫做b的倍数,b就叫做a的约数.几个整数中公有的约数,叫做这几个数的公约数:其中最大的一个,叫做这几个数的最大公约数.举例: 1 ...

  6. FLASH-CH32F203替换STM32F103 FLASH快速编程移植说明

    因CH32F203 相对于STM32 flash 操作多了快速编程模式,该文档说明主要目的是为了方便客户在原先ST 工程的基础上实现flash 快速编程模式的快速移植. 1.在stm32f10x.h ...

  7. java helloworld demo

    大二的时候写过 web 仅限于 idea 配合 springboot, 学习的时候需要写个 java demo 或者算法, 居然不知道怎么写了 首先创建一个文件夹, 写上你的代码, 因为是demo, ...

  8. Grafana 系列文章(十四):Helm 安装Loki

    前言 写或者翻译这么多篇 Loki 相关的文章了, 发现还没写怎么安装 现在开始介绍如何使用 Helm 安装 Loki. 前提 有 Helm, 并且添加 Grafana 的官方源: helm repo ...

  9. Python装饰器实例讲解(三)

    Python装饰器实例讲解(三) 本文多参考<流畅的python>,在此基础上增加了一些实例便于理解 姊妹篇 Python装饰器实例讲解(一),让你简单的会用 Python装饰器实例讲解( ...

  10. 时间轮TimeWheel工作原理解析

    时间轮工作原理解析 一.时间轮介绍 1.时间轮的简单介绍 时间轮(TimeWheel)作为一种高效率的计时器实现方案,在1987年发表的论文Hashed and Hierarchical Timing ...