本文介绍了特殊格式log日志生成自定义报告方法
0. 前言
如果在日常工作中,遇到一堆数据,但是想实现自动化将数据生成报告或者统计报表,这时候就需要掌握一些定制化输出报告的技能了;
模板文件: 对于特定格式的报告输出,首先我们需要准备一个模板文件,在这个模板文件中,需要数据的内容用变量表示,而其他与数据无关的内容则事先编辑好(主题、外观、排版等);
数据文件: 数据文件就是我们的数据来源,可以是txt文本流,可以是csv或者excel表格文件,或者其他常规数据文件;
解析器: 解析器其实就是程序或者脚本,解析器的作用是将数据文件提取转换成特定的格式,这个特定的格式是模板文件所需要注入的;
定制化报表/报告的输出流程如下:
本文所介绍的是通用的方法,可以作为其他自动化报表输出的思路参考,本文中实际使用的工程代码以及测试文件将分享在gitee上;源码
文章中的代码只是示例作用;想要熟练使用本文涉及的工具,可能要具备基本的一些技能:
Python 基本语法与基本应用
Mako文档阅读,了解Mako的语法和基本用法
1. 安装Python和Mako库
在Python环境下,安装和Mako库
2. 自定义LOG字符流(示例)
以在板单元测试的输出日志为例:
单元测试的LOG字符流输出为“UNIT:"作为前缀,
以数字开头表示测试用例下测试步骤对应的信息,如:
前缀
分隔符
步骤号
时间戳
调用的API
描述
结果
尾缀
UNIT
:
数字
浮点
spi_flash_chip_id
Read chip id is c84018
pass/fail
\r\n
信息之间用逗号分隔符分隔,如:
UNIT:0,0.1,spi_flash_chip_id,Read chip id is c84018,pass\r\n
前缀
分隔符
前缀
测试用例名称
尾缀
UNIT
:
BEGIN
ABCDEF
\r\n
表明测试用例开始执行
前缀
分隔符
前缀
测试用例名称
尾缀
UNIT
:
END
ABCDEF
\r\n
表明测试用例执行完毕
实际输出的txt文本
测试日志输出 unit_test 20241204 150444.txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 UNIT:BEGIN|FatFs Driver Test INFO:>> Flash has no filesystem yet, formatting... << INFO:>> Flash already formatted. << UNIT:0|0.719|app_fatfs_mount|fatfs can be mounted successfully|pass UNIT:1|0.729|f_open|create a new txt file|pass UNIT:1|0.785|f_write|write context to txt file|pass UNIT:1|0.936|f_close|close txt file|pass UNIT:2|0.938|f_open|open a exist txt file|pass UNIT:2|0.940|f_size|check file size 33|pass UNIT:2|0.948|f_read|read context from opened file|pass UNIT:2|0.950|f_close|close a opened file|pass UNIT:3|0.952|-|check read content is write content|fail INFO:test__Fatfs_Driver <!!!> failed: E:/MyWorkLog/MainLine2/Tools/12_PicoThermocouple/03_Embeddedsoftware/PicoTempratureMeter/unit/minunit-test-list.c:240:equal_r == FALSE UNIT:4|0.961|app_fatfs_unmount|fatfs can be unmounted successfully|pass UNIT:END|FatFs Driver Test INFO:2 tests, 20 assertions, 1 failures INFO:Finished in 0.967 seconds
3. 制作一个html报告的模板文件
模板文件可以从网上淘一个自己的喜欢的,当然,如果熟悉html也可以自己写一个;这里借用了别家html的报告稍加修改,效果大概是这样的。
测试用例下测试步骤的数据结构
这个报告的标准输出模板主体为5列表格,每一行代表了实际的测试流程项,由此我们可以用json格式先表示测试项这样的数据,以方便我们后续的代码实现:
1 2 3 4 5 6 7 { "index" : "0" , "time_stamp" : "0.719" , "api" : "app_fatfs_mount" , "description" : "fatfs can be mounted successfully" "result" : "pass" }
测试用例的数据结构
一个测试用例携带了用例信息,如样例所示,包含了测试开始时间、结束时间,测试用例标题等,一个测试用例会绑定测试步骤列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "index" : "2" , "title" : "Fatfs Driver Test" , "begin_time" : "2024-12-04 15:04:44" , "end_time" : "2024-12-04 15:04:44" , "result" : "pass" , "steps" : [ { "index" : "0" , "time_stamp" : "0.719" , "api" : "app_fatfs_mount" , "description" : "fatfs can be mounted successfully" "result" : "pass" } , { "index" : "1" , "time_stamp" : "0.729" , "api" : "f_open" , "description" : "create a new txt file" "result" : "pass" } , ] }
多个测试用例的信息即是上述结构以列表(数组)形式嵌套表述;
4. Python实现字符流解析
Python代码要实现的功能就是第二节约定的txt日志字符流文件按照特定格式解析,保存在字典数组嵌套数据里(第三节内容):
使用Python的话只需要少量的代码就可以实现解析,如上面的样例,打开txt文件,采用行读取的方式:
一次性读取所有行
逐行进行字符”:“分割,逃过前缀非”UNIT“的行
前缀为”UNIT“的行中,对后段进行字符”|“分割
"|"分割后的数据长度是固定的,如果长度是2,则表示测试用例的起始和结束;如果长度是5,则表示测试用例的测试流程信息
如果当前行是测试用例的起始,通过”BEGIN“判断,新建测试用例数据对象
如果当前行是测试流程信息,追加测试步骤信息
如果当前行是测试用例结束,通过”END“判断,追加测试用例下的完整测试步骤信息,随后将当前测试用例追加到用例列表
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 test_case_detail = {} test_case_step_detail = [] with open (self .path + "/" + item,'r' , encoding='UTF-8' ) as f: self .log_lines = f.readlines() for line in self .log_lines: line_items = line.split(':' ) if line_items[0 ].find("UNIT" ) != -1 : line_infos = line_items[1 ].split('|' ) if len (line_infos) == 5 : test_case_step_detail.append({"index" : line_infos[0 ], "time_stamp" : line_infos[1 ], "api" : line_infos[2 ], "description" : line_infos[3 ], "result" : line_infos[4 ]}) if line_infos[4 ].find("fail" ) != -1 : test_case_detail["result" ] = False else : if line_infos[0 ].find("END" ) != -1 : test_case_detail["steps" ] = test_case_step_detail test_case_detail["begin_time_stamp" ] = test_case_step_detail[0 ]["time_stamp" ] test_case_detail["end_time_stamp" ] = test_case_step_detail[-1 ]["time_stamp" ] test_cases.append(test_case_detail) if line_infos[0 ].find("BEGIN" ) != -1 : num_of_all_case += 1 test_case_detail = {"index" : num_of_all_case, "title" : line_infos[1 ], "description" : line_infos[1 ], "result" : True , "begin_time" : test_time, "end_time" : test_time} test_case_step_detail = []
5. 基于html修改成生成报告的模板文件
我们以实际输出的report.html为模板,复制重命名为template.html,找到测试步骤对应的的内容:
1 2 3 4 5 6 7 8 9 ... <tr > <td class ="DefineCell" > 0.719</td > <td class ="NumberCell" > 0</td > <td class ="DefaultCell" > app_fatfs_mount</td > <td class ="DefaultCell" > fatfs can be mounted successfully</td > <td class =PositiveResultCell > pass</td > </tr > ...
在上面对应块的地方实现Mako的渲染脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 % for step in item["steps"]: <% resCellClass = "DefaultCell" if step["result"] == "fail\n": resCellClass = "NegativeResultCell" elif step["result"] == "pass\n": resCellClass = "PositiveResultCell" endif %> <tr > <td class ="DefineCell" > ${step["time_stamp"]}</td > <td class ="NumberCell" > ${step["index"]}</td > <td class ="DefaultCell" > ${step["api"]}</td > <td class ="DefaultCell" > ${step["description"]}</td > <td class =${resCellClass} > ${step["result"]}</td > </tr > % endfor
不难发现,模板的意义就是替换掉要显示的数据内容,加以循环控制和逻辑判断;模板里的的变量,就是数据结构中的对应的内容;
6. 使用Mako对模板templete.html进行渲染
使用Mako库渲染输出报告,下面为示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import osfrom mako.template import Templatefrom mako.runtime import Contextfrom io import StringIO... mytemplate = Template(filename='./template.html' ) buf = StringIO() ctx = Context(buf, **self .test_case_detail) mytemplate.render_context(ctx) with open ("report.html" ,'w' , encoding='UTF-8' , newline="" ) as f: f.write(buf.getvalue()) ...
报告完整的效果是这样的: