You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

248 lines
9.4 KiB

package comm
import (
"testing"
"time"
)
// 周日在 Go 里 Weekday()==0、在 Dart 里 weekday==7。这套换算错了不会报错,
// 只会让「每周日」的闹钟在周一响——所以单独守住。
func TestMemoryRepeatHits_WeekdayMapping(t *testing.T) {
loc := time.UTC
// 2026-09-06 是周日,2026-09-07 是周一
sunday := time.Date(2026, 9, 6, 0, 0, 0, 0, loc)
monday := time.Date(2026, 9, 7, 0, 0, 0, 0, loc)
if sunday.Weekday() != time.Sunday || monday.Weekday() != time.Monday {
t.Fatalf("测试基准日期不对: %v %v", sunday.Weekday(), monday.Weekday())
}
// weekly + weekday=7(Dart 口径的周日)应命中周日、不命中周一
if !MemoryRepeatHits(MemoryRepeatWeekly, 7, sunday, sunday) {
t.Error("weekday=7 应命中周日")
}
if MemoryRepeatHits(MemoryRepeatWeekly, 7, monday, sunday) {
t.Error("weekday=7 不应命中周一")
}
// weekday=1 是周一
if !MemoryRepeatHits(MemoryRepeatWeekly, 1, monday, sunday) {
t.Error("weekday=1 应命中周一")
}
}
func TestMemoryRepeatHits_WeekdaysAndWeekend(t *testing.T) {
loc := time.UTC
days := map[string]time.Time{
"周五": time.Date(2026, 9, 4, 0, 0, 0, 0, loc),
"周六": time.Date(2026, 9, 5, 0, 0, 0, 0, loc),
"周日": time.Date(2026, 9, 6, 0, 0, 0, 0, loc),
"周一": time.Date(2026, 9, 7, 0, 0, 0, 0, loc),
}
anchor := days["周五"]
cases := []struct {
day string
weekday bool
weekend bool
}{
{"周五", true, false},
{"周六", false, true},
{"周日", false, true}, // 周日必须算周末,不能因为 Go 的 0 被当成周一
{"周一", true, false},
}
for _, c := range cases {
if got := MemoryRepeatHits(MemoryRepeatWeekdays, 0, days[c.day], anchor); got != c.weekday {
t.Errorf("%s weekdays 期望 %v 得到 %v", c.day, c.weekday, got)
}
if got := MemoryRepeatHits(MemoryRepeatWeekend, 0, days[c.day], anchor); got != c.weekend {
t.Errorf("%s weekend 期望 %v 得到 %v", c.day, c.weekend, got)
}
}
}
// ISO 周口径:跨年那一周最容易出错
func TestMemoryWeekKeyAndRange(t *testing.T) {
loc := time.UTC
// 2026-01-01 是周四,按 ISO 属于 2026-W01,那一周的周一是 2025-12-29
d := time.Date(2026, 1, 1, 12, 0, 0, 0, loc)
if k := MemoryWeekKey(d); k != "2026-W01" {
t.Errorf("周键期望 2026-W01 得到 %s", k)
}
start, end := MemoryWeekRange(d)
if FormatMemoryDate(start) != "2025-12-29" {
t.Errorf("周起期望 2025-12-29 得到 %s", FormatMemoryDate(start))
}
if FormatMemoryDate(end) != "2026-01-04" {
t.Errorf("周止期望 2026-01-04 得到 %s", FormatMemoryDate(end))
}
// 周日当天取到的必须还是本周(周一~周日),不能滑到下一周
sunday := time.Date(2026, 1, 4, 23, 0, 0, 0, loc)
s2, e2 := MemoryWeekRange(sunday)
if FormatMemoryDate(s2) != "2025-12-29" || FormatMemoryDate(e2) != "2026-01-04" {
t.Errorf("周日取周范围错: %s ~ %s", FormatMemoryDate(s2), FormatMemoryDate(e2))
}
}
func TestMemoryMonthRange(t *testing.T) {
loc := time.UTC
// 闰年 2 月
d := time.Date(2028, 2, 10, 0, 0, 0, 0, loc)
s, e := MemoryMonthRange(d)
if FormatMemoryDate(s) != "2028-02-01" || FormatMemoryDate(e) != "2028-02-29" {
t.Errorf("2028-02 范围错: %s ~ %s", FormatMemoryDate(s), FormatMemoryDate(e))
}
}
// 认不出的时区必须回退到本地时区,绝不能回退成 UTC——
// 那会让所有时刻静默偏移 8 小时。
func TestLoadMemoryLocation_FallbackIsLocalNotUTC(t *testing.T) {
if got := LoadMemoryLocation("Nowhere/Nothing"); got != time.Local {
t.Errorf("非法时区应回退 time.Local,得到 %v", got)
}
if got := LoadMemoryLocation(""); got != time.Local {
t.Errorf("空时区应回退 time.Local,得到 %v", got)
}
if got := LoadMemoryLocation("Asia/Tokyo"); got.String() != "Asia/Tokyo" {
t.Errorf("合法时区解析失败,得到 %v", got)
}
}
func TestNewMemoryClientKey_NotEmptyAndUnique(t *testing.T) {
seen := map[string]bool{}
for i := 0; i < 1000; i++ {
k := NewMemoryClientKey()
if k == "" {
t.Fatal("client_key 不能为空:它参与唯一索引,空串会让第二条记录插不进去")
}
if seen[k] {
t.Fatalf("client_key 重复: %s", k)
}
seen[k] = true
}
}
func TestParseMemoryClock(t *testing.T) {
if h, m, ok := ParseMemoryClock("15:30"); !ok || h != 15 || m != 30 {
t.Errorf("15:30 解析错: %d %d %v", h, m, ok)
}
if _, _, ok := ParseMemoryClock(""); ok {
t.Error("空串不该解析成功")
}
if _, _, ok := ParseMemoryClock("25:00"); ok {
t.Error("25:00 不该解析成功")
}
}
// 时区解析:IANA 名是正道,固定偏移与 CST 是给存量数据兜底的。
// ⚠️ 认不出来必须退到服务器本地时区,**不能退成 UTC**——那会让所有时刻静默偏 8 小时。
func TestLoadMemoryLocation(t *testing.T) {
sh, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Skip("镜像里没有 tzdata,跳过")
}
// 用同一个瞬间比偏移量,而不是比 Location 的名字:
// FixedZone 造出来的 loc 名字是 "UTC+8" 之类,与 "Asia/Shanghai" 永远不相等,
// 但它们在这一刻的偏移是一样的,那才是我们真正关心的东西。
at := time.Date(2026, 9, 18, 12, 0, 0, 0, time.UTC)
offsetOf := func(loc *time.Location) int {
_, off := at.In(loc).Zone()
return off
}
wantSh := offsetOf(sh)
cases := []struct {
name string
tz string
want int
}{
{"IANA 名", "Asia/Shanghai", wantSh},
{"东京", "Asia/Tokyo", 9 * 3600},
{"CST 缩写按上海解释(存量数据全是它)", "CST", wantSh},
{"cst 大小写不敏感", "cst", wantSh},
{"UTC+8", "UTC+8", 8 * 3600},
{"GMT+08:00", "GMT+08:00", 8 * 3600},
{"裸偏移 +08:00", "+08:00", 8 * 3600},
{"负偏移 -0530", "-0530", -(5*3600 + 30*60)},
{"空串 → 服务器本地", "", offsetOf(time.Local)},
{"认不出来 → 服务器本地,不是 UTC", "Mars/Olympus", offsetOf(time.Local)},
{"越界的偏移不认", "UTC+99", offsetOf(time.Local)},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := offsetOf(LoadMemoryLocation(c.tz)); got != c.want {
t.Errorf("%q 的偏移应为 %d 秒,得到 %d 秒", c.tz, c.want, got)
}
})
}
}
// 重复项展开:返回的是「除库里那一行之外还发生了几次」,不是总次数。
// 数错的后果是周报里「这周被闹钟叫了多少次」要么恒为 0(不展开),要么多算一次(不扣锚点)。
func TestMemoryExtraOccurrences(t *testing.T) {
d := func(s string) time.Time {
v, ok := ParseMemoryDate(s)
if !ok {
t.Fatalf("测试日期写错了: %s", s)
}
return v
}
// 2026-09-07(一) ~ 2026-09-13(日),典型的一个周报周期
start, end := d("2026-09-07"), d("2026-09-13")
cases := []struct {
name string
rule string
weekday int32
anchor string
want int
}{
{"一次性项不展开", MemoryRepeatOnce, 0, "2026-09-08", 0},
{"空规则不展开", "", 0, "2026-09-08", 0},
{"每天、锚点在几个月前:7 天全算,那一行没被数过", MemoryRepeatDaily, 0, "2026-06-01", 7},
{"每天、锚点就在周期内:7 次里扣掉已数过的那一行", MemoryRepeatDaily, 0, "2026-09-07", 6},
{"每天、锚点在周期中间:只从锚点当天算起", MemoryRepeatDaily, 0, "2026-09-11", 2},
{"每周一、锚点在之前:本周命中 1 次且未被数过", MemoryRepeatWeekly, 1, "2026-06-01", 1},
{"工作日、锚点在之前:5 次", MemoryRepeatWeekdays, 0, "2026-06-01", 5},
{"周末、锚点在之前:2 次", MemoryRepeatWeekend, 0, "2026-06-01", 2},
{"每月 8 号、锚点在之前:本周含 8 号,1 次", MemoryRepeatMonthly, 0, "2026-06-08", 1},
{"每月 20 号:本周不含,0 次", MemoryRepeatMonthly, 0, "2026-06-20", 0},
// ⚠️ 这条守着不能返回 -1:规则在本周期一次都没命中、但那一行恰好落在周期内时,
// 扣掉锚点会把调用方本来数对的那一行抹掉。
{"锚点在周期内但规则本周不命中:返回 0 而不是 -1", MemoryRepeatWeekly, 1, "2026-09-09", 0},
{"锚点在周期之后:一次都不算", MemoryRepeatDaily, 0, "2026-12-01", 0},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := MemoryExtraOccurrences(c.rule, c.weekday, d(c.anchor), start, end)
if got != c.want {
t.Errorf("期望 %d 次,得到 %d", c.want, got)
}
if got < 0 {
t.Error("返回负数会把调用方已经数对的那一行抹掉")
}
})
}
}
// period_start/period_end 是 type:date 列 + parseTime=True,读回 Go 的 string 字段
// 是 RFC3339 而不是 YYYY-MM-DD。任何要在 Go 里解析这个值的地方不先归一就会静默失效
// ——2026-09-18 真机上重复闹钟的展开就是这么一次都没生效的。
func TestNormalizeMemoryDate(t *testing.T) {
cases := []struct{ in, want string }{
{"2026-09-07", "2026-09-07"},
{" 2026-09-07 ", "2026-09-07"},
{"2026-09-07T00:00:00+08:00", "2026-09-07"}, // 库里读回来的真实形状
{"2026-09-07T00:00:00Z", "2026-09-07"},
{"2026-09-07 00:00:00", "2026-09-07"},
{"", ""},
{"不是日期", "不是日期"}, // 认不出来原样返回,行为与没有这个函数时一致
{"09/07/2026", "09/07/2026"},
}
for _, c := range cases {
if got := NormalizeMemoryDate(c.in); got != c.want {
t.Errorf("NormalizeMemoryDate(%q) = %q,期望 %q", c.in, got, c.want)
}
}
// 归一之后必须真的能解析出来——这才是加它的目的
if _, ok := ParseMemoryDate(NormalizeMemoryDate("2026-09-07T00:00:00+08:00")); !ok {
t.Error("归一后仍解析不出日期,重复项展开会继续静默失效")
}
}