122 lines
4.9 KiB
Go
122 lines
4.9 KiB
Go
package cmd
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"testing"
|
||
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/gcron"
|
||
"github.com/gogf/gf/v2/os/gfile"
|
||
|
||
"tool-api/internal/logic"
|
||
)
|
||
|
||
// ============================================================================
|
||
// 启动期(cmd.Main.Func)两条易静默失效/直接崩的路径,各自固化一个用例。
|
||
//
|
||
// 背景:这两条都是「本地跑起来才发现」的问题,都不会被业务单测覆盖 ——
|
||
// ① cron pattern 写错 → AddSingleton 返回 error 被 WARNING 吞掉,任务静默不注册;
|
||
// ② 静态目录不存在 → AddStaticPath 调 Logger().Fatalf → os.Exit(1),进程直接死。
|
||
// ============================================================================
|
||
|
||
// TestCronPatternsAreValid 固化启动期两个定时任务必须注册成功。
|
||
//
|
||
// GoFrame gcron 的 pattern 是 **6 段**(秒 分 时 日 月 周),不是标准 crontab 的 5 段。
|
||
// 本项目曾把「笔记提醒扫描」写成 5 段 "*/1 * * * *":
|
||
// AddSingleton 返回 error → 被启动日志里的 WARNING 吞掉 → 服务照常启动、无人察觉,
|
||
// 但该任务从未注册、笔记定时提醒功能完全失效。
|
||
func TestCronPatternsAreValid(t *testing.T) {
|
||
patterns := map[string]string{
|
||
"member-order-sweep": cronPatternSweepOrders,
|
||
"note-remind-scan": cronPatternScanRemind,
|
||
"coupon-lock-release": cronPatternReleaseCoupons,
|
||
"order-close-pending": cronPatternCloseOrders,
|
||
}
|
||
for name, pattern := range patterns {
|
||
name, pattern := name, pattern
|
||
t.Run(name, func(t *testing.T) {
|
||
if _, err := gcron.AddSingleton(
|
||
context.Background(), pattern, func(ctx context.Context) {}, name,
|
||
); err != nil {
|
||
t.Fatalf("cron 任务 %s 注册失败,pattern=%q:%v\n"+
|
||
"(gcron 需要 6 段「秒 分 时 日 月 周」;标准 5 段需前置 '#')", name, pattern, err)
|
||
}
|
||
gcron.Remove(name)
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestFivePartCronPatternIsRejected 反证上面的用例不是空转:
|
||
// 标准 5 段 crontab 写法确实会被 gcron 拒绝。
|
||
// 若哪天这条断言失效(gcron 升级后支持了 5 段),说明本文件的假设已过期,需重新核对。
|
||
func TestFivePartCronPatternIsRejected(t *testing.T) {
|
||
const bad = "*/1 * * * *"
|
||
if _, err := gcron.AddSingleton(
|
||
context.Background(), bad, func(ctx context.Context) {}, "probe-bad-5part",
|
||
); err == nil {
|
||
t.Fatalf("5 段 pattern %q 竟被 gcron 接受,本文件假设已失效,请重新核对 gcron 行为", bad)
|
||
}
|
||
gcron.Remove("probe-bad-5part")
|
||
}
|
||
|
||
// TestQrCodeStaticPathBootOrder 固化「静态目录必须先存在」这一启动约束。
|
||
//
|
||
// 机理(已读 gf 源码确认,net/ghttp/ghttp_server_config_static.go:93):
|
||
//
|
||
// if p, err := gfile.Search(path); err != nil {
|
||
// s.Logger().Fatalf(ctx, `AddStaticPath failed: %+v`, err)
|
||
// }
|
||
//
|
||
// 是 Fatalf → os.Exit(1),**不是 panic、无法 recover**。
|
||
// 而 manifest/qrcode 原先只在「生成小程序码」时才惰性创建 →
|
||
// 全新环境首次启动必然崩在注册静态路由这一步(先有鸡先有蛋的死锁)。
|
||
// 修法:cmd.go 先 gfile.Mkdir(logic.QrCodeDir) 再 AddStaticPath。
|
||
func TestQrCodeStaticPathBootOrder(t *testing.T) {
|
||
chdirRepoRoot(t)
|
||
|
||
// ① 目录不存在时 gfile.Search 会报错(这就是上面 Fatalf 的触发条件)。
|
||
// 用 Search 探测而不真的调 AddStaticPath —— 后者会直接 os.Exit(1) 杀掉测试进程。
|
||
t.Run("missing_dir_makes_gf_search_fail", func(t *testing.T) {
|
||
if _, err := gfile.Search("manifest/__definitely_absent__"); err == nil {
|
||
t.Error("gfile.Search 对不存在的目录竟返回成功,本用例假设已失效(gf 行为可能已变更)")
|
||
}
|
||
})
|
||
|
||
// ② 预创建目录后,AddStaticPath 可以正常注册(即 cmd.go 的修复路径)。
|
||
// AddStaticPath 无返回值,能走到下一行即为通过。
|
||
t.Run("precreated_dir_registers_ok", func(t *testing.T) {
|
||
if err := gfile.Mkdir(logic.QrCodeDir); err != nil {
|
||
t.Fatalf("gfile.Mkdir(%q) 失败:%v", logic.QrCodeDir, err)
|
||
}
|
||
if !gfile.Exists(logic.QrCodeDir) {
|
||
t.Fatalf("Mkdir 之后目录仍不存在:%q", logic.QrCodeDir)
|
||
}
|
||
g.Server("static-path-probe").AddStaticPath("/qrcode", logic.QrCodeDir)
|
||
})
|
||
}
|
||
|
||
// chdirRepoRoot 把工作目录切到仓库根目录。
|
||
// go test 的 CWD 是「包目录」(internal/cmd),直接用相对路径会跑偏到 internal/cmd/manifest/qrcode,
|
||
// 与被测代码在真实运行时的 CWD(仓库根)不一致。
|
||
func chdirRepoRoot(t *testing.T) {
|
||
t.Helper()
|
||
old, err := os.Getwd()
|
||
if err != nil {
|
||
t.Fatalf("获取当前工作目录失败:%v", err)
|
||
}
|
||
t.Cleanup(func() { _ = os.Chdir(old) })
|
||
|
||
dir := old
|
||
for i := 0; i < 8; i++ {
|
||
if gfile.Exists(gfile.Join(dir, "go.mod")) {
|
||
if err = os.Chdir(dir); err != nil {
|
||
t.Fatalf("切换到仓库根目录 %s 失败:%v", dir, err)
|
||
}
|
||
return
|
||
}
|
||
dir = gfile.Dir(dir)
|
||
}
|
||
t.Fatal("向上 8 层未找到 go.mod,无法定位仓库根目录")
|
||
}
|