本文介绍了特殊格式log日志生成自定义报告方法

0. 前言

如果在日常工作中,遇到一堆数据,但是想实现自动化将数据生成报告或者统计报表,这时候就需要掌握一些定制化输出报告的技能了;

  • 模板文件: 对于特定格式的报告输出,首先我们需要准备一个模板文件,在这个模板文件中,需要数据的内容用变量表示,而其他与数据无关的内容则事先编辑好(主题、外观、排版等);
  • 数据文件: 数据文件就是我们的数据来源,可以是txt文本流,可以是csv或者excel表格文件,或者其他常规数据文件;
  • 解析器: 解析器其实就是程序或者脚本,解析器的作用是将数据文件提取转换成特定的格式,这个特定的格式是模板文件所需要注入的;

定制化报表/报告的输出流程如下:

本文所介绍的是通用的方法,可以作为其他自动化报表输出的思路参考,本文中实际使用的工程代码以及测试文件将分享在gitee上;源码
文章中的代码只是示例作用;想要熟练使用本文涉及的工具,可能要具备基本的一些技能:

  • Python 基本语法与基本应用
  • Mako文档阅读,了解Mako的语法和基本用法

1. 安装Python和Mako库

在Python环境下,安装和Mako库

1
pip install 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 os
from mako.template import Template
from mako.runtime import Context
from 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())
...

报告完整的效果是这样的: