pytest-live-report 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2026-09-13
3
+ # @Author : Vsoapmac
4
+ # @File : __init__.py
5
+ # @Software: VSCode
6
+ # @Description: pytest-live-report 包入口与公开别名
7
+
8
+ """pytest-live-report: 边跑边写的 pytest HTML 报告
9
+
10
+ 测试用例里只需要 `from pytest_live_report import live_report`, 然后调 `live_report.log()`.
11
+ 插件本身由 `pyproject.toml` 里的 pytest11 entry point 自动加载, 用户不用配 conftest.
12
+ """
13
+
14
+ # ------------ this package ------------
15
+ from ._report import live_report
16
+
17
+ __version__ = "0.2.0"
18
+
19
+ # 字母序, 只列公开名字
20
+ __all__ = [
21
+ "__version__",
22
+ "live_report",
23
+ ]
24
+
@@ -0,0 +1,148 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2026-09-13
3
+ # @Author : Vsoapmac
4
+ # @File : _case.py
5
+ # @Software: VSCode
6
+ # @Description: 一条用例的数据模型, 不碰 HTML
7
+
8
+ """存放一条用例的展示数据, 并提供两个解析 pytest 输出的小工具
9
+
10
+ 卡片长什么样归 `_render.case`, 这个模块既不知道 HTML 也不知道 CSS 类名, 所以
11
+ 钩子层与状态层都可以放心依赖它.
12
+ """
13
+
14
+ # ------------ standard library ------------
15
+ import json
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime
18
+ from typing import List, Optional
19
+
20
+ # ------------ constants ------------
21
+ # 用例的三种状态, 也是页面筛选按钮的三个取值
22
+ STATUS_PASSED = "passed"
23
+ STATUS_FAILED = "failed"
24
+ STATUS_SKIPPED = "skipped"
25
+ # pytest 给跳过原因加的前缀 (`pytest.skip("x")` 会写成 "Skipped: x"), 展示前剥掉,
26
+ # 否则卡片上会重复出现 Skipped
27
+ _SKIP_PREFIX = "Skipped: "
28
+
29
+
30
+ # region ---------------------------- 用例数据 ----------------------------
31
+ @dataclass
32
+ class CaseData:
33
+ """一条用例的全部展示信息
34
+
35
+ 用例开始时先建好骨架 (nodeid / name / desc / started), 执行期间由 `report.*`
36
+ 往里追加日志, 收尾时钩子层再补上状态, 耗时与错误信息.
37
+ """
38
+
39
+ # pytest 的完整节点 id, 例如 tests/test_a.py::test_login
40
+ nodeid: str = ""
41
+ # 用例函数名, 作为卡片主标题
42
+ name: str = ""
43
+ # 用例函数的 docstring, 显示在卡片详情区的顶部; 没有就是空串
44
+ desc: str = ""
45
+ # passed / failed / skipped 之一
46
+ status: str = STATUS_PASSED
47
+ # 开始与结束时刻 (带本地时区); 取不到时是 None, 卡片上对应位置留空
48
+ started: Optional[datetime] = None
49
+ finished: Optional[datetime] = None
50
+ # 三个阶段的耗时之和, 单位是秒, 已保留两位小数
51
+ duration: float = 0.0
52
+ # 跳过原因, 只有跳过用例非空; 已经剥掉 pytest 加的前缀
53
+ skip_reason: str = ""
54
+ # 错误摘要, 例如 "AssertionError: 500 != 200"; 通过用例是空串
55
+ error: str = ""
56
+ # 完整 traceback 原文; 通过用例是空串
57
+ traceback: str = ""
58
+ # 用例执行期间写下的日志行, 按调用顺序; 每项是
59
+ # (时间戳, 原始文本, 已转义的 HTML 片段, 是否带样式)
60
+ logs: List[tuple] = field(default_factory=list)
61
+ # 用例执行期间保存的截图, 按调用顺序; 每项是
62
+ # (data URI, 图注或空串, 图片字节数, MIME 类型)
63
+ shots: List[tuple] = field(default_factory=list)
64
+
65
+ def json_record(self) -> dict:
66
+ """取这条用例的 JSON 记录, 供脚本提取执行信息
67
+
68
+ 这里只产出数据, 拼成页内 `<script>` 标签归 `_render.case`.
69
+
70
+ `v` 是版本号: 加字段不动它, 改字段名, 删字段或改动字段含义才递增. 所以脚本
71
+ 读可选字段要用 `record.get("logs", [])`, 键缺失按空处理, 新旧报告都吃得下.
72
+
73
+ Returns:
74
+ dict: 含 nodeid / name / status / duration / 起止时间 / 错误与跳过原因,
75
+ 以及日志与截图的摘要; 图片数据不在这里, 只留在卡片 HTML 里
76
+
77
+ Example:
78
+ >>> CaseData(nodeid="t.py::test_x", status="passed").json_record()["status"]
79
+ 'passed'
80
+ """
81
+ return {
82
+ "v": 1,
83
+ "nodeid": self.nodeid,
84
+ "name": self.name,
85
+ "desc": self.desc,
86
+ "status": self.status,
87
+ "duration": self.duration,
88
+ "started": "" if self.started is None else self.started.isoformat(),
89
+ "finished": "" if self.finished is None else self.finished.isoformat(),
90
+ "error": self.error,
91
+ "traceback": self.traceback,
92
+ "skip_reason": self.skip_reason,
93
+ # 普通文本的 html 与 text 完全相同, 不必再存一份
94
+ "logs": [
95
+ {"ts": stamp, "text": text, "html": html}
96
+ if styled
97
+ else {"ts": stamp, "text": text}
98
+ for stamp, text, html, styled in self.logs
99
+ ],
100
+ # 只给摘要: 脚本靠这几项就能核对截图存进来了没有
101
+ "shots": [
102
+ {"caption": note, "bytes": size, "mime": mime}
103
+ for _uri, note, size, mime in self.shots
104
+ ],
105
+ }
106
+ # endregion ---------------------------- 用例数据 ----------------------------
107
+
108
+
109
+ # region ---------------------------- 结果解析 ----------------------------
110
+ def strip_skip_prefix(reason: str) -> str:
111
+ """去掉 pytest 给跳过原因加的 `Skipped: ` 前缀
112
+
113
+ Args:
114
+ reason (str): pytest 给的原始跳过原因
115
+
116
+ Returns:
117
+ str: 去掉前缀的原因; 本来就没有前缀时原样返回
118
+
119
+ Example:
120
+ >>> strip_skip_prefix("Skipped: service unavailable")
121
+ 'service unavailable'
122
+ >>> strip_skip_prefix("plain reason")
123
+ 'plain reason'
124
+ """
125
+ if reason.startswith(_SKIP_PREFIX):
126
+ return reason[len(_SKIP_PREFIX):]
127
+ return reason
128
+
129
+
130
+ def param_suffix(nodeid: str) -> str:
131
+ """取 nodeid 末尾的参数化后缀, 例如 `test_login[admin]` 里的 `[admin]`
132
+
133
+ Args:
134
+ nodeid (str): 用例的完整节点 id
135
+
136
+ Returns:
137
+ str: 形如 `[admin]` 的后缀; nodeid 里没有方括号段时是空串
138
+
139
+ Example:
140
+ >>> param_suffix("tests/test_a.py::test_login[admin]")
141
+ '[admin]'
142
+ >>> param_suffix("tests/test_a.py::test_login")
143
+ ''
144
+ """
145
+ tail = nodeid.rsplit("::", 1)[-1]
146
+ start = tail.find("[")
147
+ return tail[start:] if start != -1 else ""
148
+ # endregion ---------------------------- 结果解析 ----------------------------