与 Coveralls 的不解之缘

开发 后端
如果是手工统计的,肯定都往高了写,这样的数据也就没有价值,如果是程序自动测试出来的,想着都觉得复杂,是怎么实现的呢?带着这些疑问,我点击了那个 coverage 98%,跳转到了 https://coveralls.io/ 的页面。

[[403562]]

前两天在 GitHub 浏览 Python 的三方库时,看到了以下

就像 https 那个绿色锁的标志一样,看着很可信,让人用着放心,很多开源项目都有这些图标。

看到 coverage 是 98%,我产生了疑问,这是手工统计的,还是程序自动测试出来的呢?

如果是手工统计的,肯定都往高了写,这样的数据也就没有价值,如果是程序自动测试出来的,想着都觉得复杂,是怎么实现的呢?带着这些疑问,我点击了那个 coverage 98%,跳转到了 https://coveralls.io/ 的页面。

探索了一番,发现原来这是叫 coveralls 的三方库实现的,用于在线实时显示单元测试的覆盖率,测试数据是通过 coverage 来跑出来的。

好奇的我 pip install 安装了下,拿自己之前的程序,写了几个单元测试,用了下这两条命令:

  1. coverage run --source=dbinterface -m pytest tests/ 
  2. coverage report -m 

发现,这个单元测试的覆盖率果然是程序自动统计出来的,coverage 真的太牛了,有了这个,写单元测试就无法偷懒了,代码质量就有了量化标准。

从上面的图中可以看到文件的哪些代码行没有测试到,然后针对性的编写单元测试。还可以生成 html 文件进行查询,更为直观。

猜测 coverage 应该是记录了 pytest 调用的代码行数,然后从全部代码行记录中去除已经测试过的行记录,就是未测试的代码行,从而统计覆盖率。

当时,我不由自主发出了‘卧槽牛批’,不过仍然有疑问,程序是怎么检测哪些代码行被执行了呢?虽然我知道 debug 时可以看到,但是如何写程序统计,我还一无所知。

好奇心驱使着我去探索。

首先看下这个 coverage 来自哪里,里面有什么内容:

  1. (py38env) ? dbinterface git:(master) ? which coverage 
  2.  
  3. /Users/aaron/py38env/bin/coverage 

可以看到 coverage 的内容:

  1. (py38env) ➜  dbinterface git:(master) ✗ cat /Users/aaron/py38env/bin/coverage 
  2. #!/Users/aaron/py38env/bin/python3 
  3. # -*- coding: utf-8 -*- 
  4. import re 
  5. import sys 
  6. from coverage.cmdline import main 
  7. if __name__ == '__main__'
  8.     sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$''', sys.argv[0]) 
  9.     sys.exit(main()) 
  10. (py38env) ➜  dbinterface git:(master) ✗ 

其实,在命令行执行 coverage,就相当于执行:

  1. /Users/aaron/py38env/bin/python3 coverage 

将该文件保存到一个目录中,命名为 main.py,然后使用 PyCharm IDE 开始调试,调试的过程中,发现 coverage run --source=dbinterface -m pytest tests/ 命令会将测试的结果写入到文件 .coverage 中,再执行 coverage report -m 时会从该文件统计出覆盖率。

也就是说关键是要弄清楚命令 coverage run --source=dbinterface -m pytest tests/ 的执行过程。

继续 Debug,这里说下,由于我们的命令是在路径 /Users/aaron/github/somenzz/dbinterface 下执行的,在 Debug 前,先使用 os.chdir 改变程序的工作目录:

main.py

  1. #!/Users/aaron/py38env/bin/python3 
  2. # -*- coding: utf-8 -*- 
  3. import re 
  4. import sys 
  5. from coverage.cmdline import main 
  6. import os 
  7. if __name__ == '__main__'
  8.     os.chdir('/Users/aaron/github/somenzz/dbinterface'
  9.     sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$''', sys.argv[0]) 
  10.     print(sys.argv[0]) 
  11.     sys.exit(main()) 

然后加入参数,打好断点,追踪。

最后追踪到这里:

这里的 tracer 类就是 CTracer,其源头可以从 collector.py 文件的这段代码看出来:

可以看出 tracer 的原型要么是 CTracer, 要么是 PyTracer。

从作者的注释中可以看到 CTracer 速度非常快,而 PyTracer 相对较慢。

想看 CTracer 的源代码,结果发现了这个文件

.so 文件相当于 windows 的 dll 文件,是动态链接库,需要反编译成汇编语言,然后再分析执行逻辑,这个对我来说太难了,自己又不熟悉汇编,于是放弃。

那就只剩下 PyTracer 了,原理应该是类似的,PyTracer 的源代码就是 pytracer.py 文件,可以直接打开查看。

文件开始的地方,导入了以下三个库:

  1. import atexit 
  2. import dis 
  3. import sys 
  4.  
  5. from coverage import env 

其中前三个都是标准库,atexit 是退出处理器,可以注册一个函数,在解释器终止时执行。dis 是 Python 字节码反汇编器,这两个的使用只有一次,没有派上大用处,可以忽略。

重点就是第三个 sys 模块,这个模块和 os 模块可以说是博大精深,很多程序都会使用到,从包的名称也可以总结规律,名字越短,就越重要,其使用频率就越高。

看 PyTracer 源代码, sys.settrace 是起决定作用的,是 coverage 能够统计单元测试覆盖率的关键。

下面是对 Python 官方文档对 sys.settrace 的介绍:

sys.settrace(tracefunc) 用来设置系统的跟踪函数,使得用户在 Python 中就可以实现 Python 源代码调试器。该函数是特定于单个线程的,所以要让调试器支持多线程,必须为正在调试的每个线程都用 settrace() 注册一个跟踪函数,或使用 threading.settrace()。

跟踪函数应接收三个参数:frame、event 和 arg。frame 是当前的堆栈帧。event 是一个字符串:'call'、'line'、'return'、'exception' 或 'opcode'。arg 取决于事件类型。

官方文档 bb 这么多,说实话我也没太懂,到底咋用呢?我网上找了一个例子,比如说文件 trace.py 内容如下:

  1. import sys 
  2.  
  3. def stuff(): 
  4.     print("calling stuff"
  5.  
  6. def printer(frame, event, arg): 
  7.     print(frame, event, arg) 
  8.     return printer # return itself to keep tracing 
  9.  
  10. sys.settrace(printer) 
  11. stuff() 

也就是说执行函数之前,加上 sys.settrace。执行该文件,可以得到以下结果:

  1. (py38env) ➜  tmp python trace.py 
  2. <frame at 0x7fa6bff5a440, file 'trace.py', line 3, code stuff> call None 
  3. <frame at 0x7fa6bff5a440, file 'trace.py', line 4, code stuff> line None 
  4. calling stuff 
  5. <frame at 0x7fa6bff5a440, file 'trace.py', line 4, code stuff> return None 
  6. (py38env) ➜  tmp 

程序执行的行数,执行的操作都完整的显示了出来,将这些数据保存到文件中,就可以进行单元测试覆盖率的统计了。

虽然无法方便的查询 CTracer 源码,但是从 PyTracer 这里还是学习到了 coverage 统计单元测试覆盖率的统计方法。

一次偶遇 coveralls 让我见识了 Python 原来还可以统计代码的执行情况,真的太秀了。

趁热打铁,我用 coveralls 的状态图标也发布了一个工具库:dbinterface,单元测试覆盖率 89%:

这个一个数据库操作的通用接口,使用起来是相当的简单,从此读写各种数据库都不是事:

  1. from dbinterface.database_client import DataBaseClientFactory 
  2.  
  3. client1 = DataBaseClientFactory.create
  4.             dbtype="postgres"
  5.             host="localhost"
  6.             port=5432, 
  7.             user="postgres"
  8.             pwd="121113"
  9.             database="postgres"
  10.         ) 
  11.  
  12. client2 = DataBaseClientFactory.create
  13.             dbtype="mysql"
  14.             host="localhost"
  15.             port=3306, 
  16.             user="aaron"
  17.             pwd="aaron"
  18.             database="information_schema"
  19.         ) 
  20.  
  21. result1 = client1.read("select current_date"
  22. rows_affeted = client1.write( 
  23.     "insert into tmp_test_table values(%s, %s)", ("1""aaron"
  24. rows_export = client.export( 
  25.             "select * from information_schema.TABLES"
  26.             params=(), 
  27.             file_path="/Users/aaron/tmp/mysql_tables.txt"
  28.             delimeter="0x02"
  29.             quote="0x03"
  30.             all_col_as_str=False
  31.         ) 
  32.  
  33. assert rows_export > 0 

项目地址:https://github.com/somenzz/dbinterface

本文转载自微信公众号「Python七号」,可以通过以下二维码关注。转载本文请联系Python七号公众号。

 

责任编辑:武晓燕 来源: Python七号
相关推荐

2010-03-15 18:03:18

Java线程

2010-03-18 19:06:35

Java socket

2023-11-07 12:30:38

数据结构红黑树

2016-05-27 11:43:06

2014-10-30 17:43:59

Android 5.0Android Wea

2013-03-26 10:27:32

社交游戏公司云存储

2009-02-19 09:48:34

XP微软降级

2012-05-24 21:36:44

苹果

2021-08-02 10:40:45

机器人人工智能算法

2018-06-19 09:54:22

MySQLHBase存储

2016-09-30 10:30:12

2018-11-13 14:41:35

溯源区块链商场

2017-09-20 14:07:44

2015-07-03 13:38:42

廖厂长

2014-10-30 14:47:37

2016-12-12 14:05:29

戴尔

2010-03-10 17:43:41

Python编程语言

2016-07-18 10:36:22

华为

2009-11-06 09:39:40

WCF契约

2017-04-11 20:37:25

虚拟化存储网络
点赞
收藏

51CTO技术栈公众号