enhance(tests): 改进测试用例记录与覆盖率报告

- 在 ApiCase 中添加失败原因记录,提升测试结果的可读性
- 更新 CoverageReport 结构,增加总用例、通过用例和失败用例计数
- 优化 PrintCoverage 方法,支持详细输出未通过接口及其失败原因
- 更新 Swagger 生成逻辑,包含失败原因字段,增强调试信息
- 改进 SQL 错误计数逻辑,确保在测试模式下准确记录 SQL 错误
This commit is contained in:
2026-04-13 06:45:16 +08:00
parent 9a9b9c83ff
commit 3430bdec19
9 changed files with 516 additions and 151 deletions
@@ -0,0 +1,139 @@
---
name: Fix caller detection
overview: 修复三个日志/测试相关 bug:(1) 调用者智能识别漏掉 log/ 包自身 (2) 测试模式 SQL 出错不会导致用例失败 (3) 测试模式 WebConnectLog 写入已关闭的 NUL 文件
todos:
- id: fix-framework-detection
content: 在 isHoTimeFrameworkFile 中 runtime/ 检查之后,添加 log/ 前缀检查
status: completed
- id: fix-sql-error-test-fail
content: 测试模式 SQL 出错时自动让当前用例失败(db 增加错误计数 + execute 中检查)
status: completed
- id: fix-zerolog-nul-closed
content: NewTestApp 中 Init 完成后将 WebConnectLog 置为 nil,避免写入已关闭的 NUL 文件
status: completed
isProject: false
---
# 修复日志/测试三个 Bug
---
## Bug 1:调用者智能识别始终显示 log/logger.go:318
### 根因
[log/logger.go](log/logger.go) 中的 `isHoTimeFrameworkFile` 函数对 `log/` 包自身存在识别盲区。
```mermaid
flowchart TD
A["应用代码 或 db/query.go 调用 Logger.Debug()"] --> B["zerolog Event.Msg()"]
B --> C["callerMarshalFunc (log/logger.go:317)"]
C --> D["findCaller 通过 runtime.Caller(i) 逐帧遍历调用栈"]
D --> E{"isHoTimeFrameworkFile?"}
E -->|"是框架文件 -> 跳过,继续向上"| D
E -->|"非框架文件 -> 作为调用者返回"| F["显示在日志输出中"]
```
`findCaller``i=1` 时拿到 `log/logger.go:318`。经过 `shortenPath` 截断后变成 `log/logger.go`。两条检测路径都漏掉了它:
- **路径一(第 392 行)**`frameworkDirs` 包含 `"log/"`,但要求路径含 `"hotime"``shortenPath` 只保留 2 段路径,`log/logger.go` 中没有 `"hotime"`。**未命中。**
- **路径二(第 410 行)**`frameworkCoreDirs` 只有 `db/``common/``code/``cache/`**没有 `"log/"`**。**未命中。**
### 修复
在 [log/logger.go](log/logger.go) 第 387-389 行 `runtime/` 检查之后加入 `log/` 前缀检查:
```go
if strings.HasPrefix(file, "runtime/") {
return true
}
if strings.HasPrefix(file, "log/") {
return true
}
```
---
## Bug 2:测试模式 SQL 执行失败不会导致用例失败
### 根因
[db/query.go](db/query.go) `logSQL` 在测试模式下只是把 SQL 错误写入 `testLogBuf`,但 [testing_api.go](testing_api.go) 的 `execute` 中只在断言失败时才打印这些日志,**从不检查是否有 SQL 错误**。即使请求处理过程中数据库出错,只要 HTTP 响应的 status/msg 符合预期,测试就通过了。
当前流程:
```mermaid
flowchart LR
A["ServeHTTP"] --> B["检查 status/msg/result"]
B -->|"通过"| C["FlushTestLog 丢弃"]
B -->|"失败"| D["FlushTestLog 打印"]
```
### 修复
在 [db/db.go](db/db.go) 增加一个测试 SQL 错误计数器:
```go
testSQLErrCount int32 // 测试模式下 SQL 错误计数(atomic)
```
在 [db/query.go](db/query.go) `logSQL` 中,当 `err != nil``testLogBuf != nil` 时,`atomic.AddInt32(&that.testSQLErrCount, 1)`
在 [db/db.go](db/db.go) 增加两个方法:
```go
func (that *HoTimeDB) TestSQLErrorCount() int32 {
return atomic.LoadInt32(&that.testSQLErrCount)
}
func (that *HoTimeDB) ResetTestSQLErrorCount() {
atomic.StoreInt32(&that.testSQLErrCount, 0)
}
```
在 [testing_api.go](testing_api.go) `execute` 中,`ServeHTTP` 之后、断言之前,检查计数:
```go
c.api.app.Db.FlushTestLog() // 先清空请求前的残留
// ... ServeHTTP ...
sqlErrCount := c.api.app.Db.TestSQLErrorCount()
c.api.app.Db.ResetTestSQLErrorCount()
if sqlErrCount > 0 {
sqlLog := c.api.app.Db.FlushTestLog()
t.Fatalf("请求处理中有 %d 次 SQL 错误:\n%s", sqlErrCount, sqlLog)
}
```
修复后流程:
```mermaid
flowchart LR
A["ServeHTTP"] --> B{"SQL 出错?"}
B -->|"是"| C["t.Fatalf 用例直接失败"]
B -->|"否"| D["正常断言 status/msg/result"]
```
---
## Bug 3zerolog: could not write event: write NUL: file already closed
### 根因
[testing_helper.go](testing_helper.go) `NewTestApp` 中的初始化顺序:
```
1. os.Stderr = devNull (重定向到 NUL)
2. Init(configPath) (创建 WebConnectLog,其 ConsoleWriter.Out = devNull)
3. os.Stderr = origStderr (恢复真实 stderr)
4. devNull.Close() (关闭 NUL 文件)
5. app.Log.SetOutput(io.Discard) (只修复了主日志)
-- WebConnectLog 仍然引用已关闭的 devNull --
6. 测试请求触发 WebConnectLog.Info() → 写入已关闭文件 → 报错
```
### 修复
在 [testing_helper.go](testing_helper.go) `devNull.Close()` 之后、`app.Log.SetOutput(io.Discard)` 之前,加一行:
```go
app.WebConnectLog = nil
```
测试环境不需要访问日志。`application.go` 第 350 行 `if that.WebConnectLog != nil` 已有 nil 保护,置 nil 即可安全跳过。
@@ -0,0 +1,127 @@
---
name: Fix caller detection v2
overview: 重新设计 findCaller 的调用者过滤逻辑:(1) 删除 maxFrameworkDepth 提前退出 (2) 收窄 isHoTimeFrameworkFile 定义为仅跳过日志/DB执行管线,让框架业务文件(code/makecode.go、application.go、context.go 等)作为有意义的调用者返回。
todos:
- id: rewrite-findcaller
content: 重写 findCaller:删除 maxFrameworkDepth、applicationFile 跟踪,简化为跳过基础设施 + 返回第一个业务文件
status: completed
- id: rewrite-framework-check
content: 重命名 isHoTimeFrameworkFile 为 isInfrastructureFile,收窄到只匹配 log/db/cache/common/dri,移除 code/ 和 application.go 等
status: completed
isProject: false
---
# 修复调用者智能识别(v2
## 根因分析
用户看到 `[db/crud.go:149]`,实际调用者是 `code/makecode.go`。两个问题叠加:
**问题 A**: `maxFrameworkDepth = 10` 过低。zerolog 内部帧(~5) + log/logger.go(1) + db/query.go(3-4) + db/crud.go(1) 轻松达到 10,触发提前返回,根本没走到 `code/makecode.go`
**问题 B**: 即使去掉 `maxFrameworkDepth``code/makecode.go``frameworkCoreDirs` 中被 `"code/"` + `"makecode.go"` 命中,也会被跳过。最终回退到 `applicationFile`application.go)或 `lastFrameworkFile`,都不是用户想要的。
## 设计思路
用户原话:"应用层触发的就打印应用层的,如果是框架层的业务造成的就打印框架层对应位置的,但肯定不是 log/logger.go"。
核心区分:
- **基础设施文件**(应跳过):日志管线(log/、zerolog/)、DB 执行管线(db/\*)、缓存中间件(cache/\*)、Go runtime、通用工具(common/\*)、数据库驱动(dri/\*
- **业务文件**(应返回):`code/makecode.go``application.go``context.go``session.go`、所有应用层代码
跳过基础设施,返回第一个业务文件。无论这个业务文件是应用层还是框架层。
## 具体修改
### 1. [log/logger.go](log/logger.go) -- `findCaller` 简化
- **删除** `maxFrameworkDepth` 常量(第 315 行)和 `frameworkCount >= maxFrameworkDepth` 提前退出(第 343-345 行)。`i < 20` 循环上限已足够保护。
- **删除** `applicationFile` / `applicationLine` 跟踪及其回退逻辑(不再需要 application.go 特殊处理)。
- **删除** `frameworkCount` 计数器(不再需要)。
- 简化后的 `findCaller`
```go
func findCaller(origFile string, origLine int) string {
var lastInfraFile string
var lastInfraLine int
for i := 1; i < 20; i++ {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
}
shortFile := shortenPath(file)
if isInfrastructureFile(shortFile) {
lastInfraFile = shortFile
lastInfraLine = line
continue
}
return fmt.Sprintf("%s:%d", shortFile, line)
}
if lastInfraFile != "" {
return fmt.Sprintf("%s:%d", lastInfraFile, lastInfraLine)
}
return fmt.Sprintf("%s:%d", shortenPath(origFile), origLine)
}
```
### 2. [log/logger.go](log/logger.go) -- 重命名 + 收窄 `isHoTimeFrameworkFile`
重命名为 `isInfrastructureFile`,仅匹配日志/DB/缓存/通用工具等"管道"文件:
```go
func isInfrastructureFile(file string) bool {
// 第三方日志库
if strings.HasPrefix(file, "zerolog/") || strings.HasPrefix(file, "zerolog@") {
return true
}
if strings.HasPrefix(file, "logrus/") || strings.HasPrefix(file, "logrus@") {
return true
}
if strings.HasPrefix(file, "runtime/") {
return true
}
// HoTime 日志包
if strings.HasPrefix(file, "log/") {
return true
}
// DB 执行管线(db/ 下全部文件)
infraPrefixes := []string{"db/", "cache/", "common/", "dri/"}
for _, prefix := range infraPrefixes {
if strings.HasPrefix(file, prefix) {
return true
}
}
// 兼容路径含 hotime 的场景(module 全路径未被 shortenPath 完全截断时)
lowerFile := strings.ToLower(file)
if strings.Contains(lowerFile, "hotime") {
infraDirs := []string{"db/", "cache/", "common/", "log/", "dri/"}
for _, dir := range infraDirs {
if strings.Contains(file, dir) {
return true
}
}
}
return false
}
```
关键变更:
- `"code/"` 从所有列表中移除 -- `code/makecode.go` 不再被跳过
- `application.go``context.go``session.go` 等单文件匹配全部移除 -- 框架业务文件不再被跳过
- `frameworkCoreDirs` 中的 `"code/"` 及其文件名(makecode.go、template.go、config.go)移除
## 修复后效果
| 调用链 | 显示结果 |
|--------|---------|
| app/user.go -> db.GetMap -> logSQL | `app/user.go:XX` |
| code/makecode.go -> db.GetList -> logSQL | `code/makecode.go:XX` |
| application.go SetConfig -> db -> logSQL | `application.go:XX` |
| context.go session -> db -> logSQL | `context.go:XX` |