更新日历功能

This commit is contained in:
2026-09-24 15:14:30 +08:00
parent c5ed596008
commit bc9e32d62a
13 changed files with 1446 additions and 6 deletions
+229
View File
@@ -0,0 +1,229 @@
package holiday
import (
"testing"
"time"
)
// TestFallbackCovers2026 校验内置兜底数据与国务院办公厅 2026 年通知一致。
// 这里的期望值直接抄自官方通知,属于"事实断言":一旦有人改错数据即失败。
func TestFallbackCovers2026(t *testing.T) {
got := builtinByYear(2026)
if len(got) == 0 {
t.Fatal("2026 年内置数据为空")
}
byDate := make(map[string]Day, len(got))
for _, d := range got {
byDate[d.Date] = d
}
// 元旦 1/1-1/3 放假;1/4 上班
for _, d := range []string{"2026-01-01", "2026-01-02", "2026-01-03"} {
if v, ok := byDate[d]; !ok || v.Type != TypeHoliday {
t.Errorf("%s 应为放假日,实际 %+v", d, v)
}
}
if v, ok := byDate["2026-01-04"]; !ok || v.Type != TypeWorkday {
t.Errorf("2026-01-04 应为调休上班日,实际 %+v", v)
}
// 春节 2/15-2/23 放假,共 9 天;2/14、2/28 上班
springCount := 0
for _, d := range got {
if d.Type == TypeHoliday && d.Name == "春节" {
springCount++
}
}
if springCount != 9 {
t.Errorf("春节放假天数应为 9,实际 %d", springCount)
}
for _, d := range []string{"2026-02-14", "2026-02-28"} {
if v, ok := byDate[d]; !ok || v.Type != TypeWorkday {
t.Errorf("%s 应为春节调休上班日,实际 %+v", d, v)
}
}
// 国庆 10/1-10/7 放假;9/20、10/10 上班
nationalCount := 0
for _, d := range got {
if d.Type == TypeHoliday && d.Name == "国庆节" {
nationalCount++
}
}
if nationalCount != 7 {
t.Errorf("国庆放假天数应为 7,实际 %d", nationalCount)
}
for _, d := range []string{"2026-09-20", "2026-10-10"} {
if v, ok := byDate[d]; !ok || v.Type != TypeWorkday {
t.Errorf("%s 应为国庆调休上班日,实际 %+v", d, v)
}
}
// 中秋 9/25-9/27、端午 6/19-6/21、清明 4/4-4/6、劳动节 5/1-5/5
cases := []struct {
name string
start string
count int
}{
{"清明节", "2026-04-04", 3},
{"劳动节", "2026-05-01", 5},
{"端午节", "2026-06-19", 3},
{"中秋节", "2026-09-25", 3},
}
for _, c := range cases {
n := 0
for _, d := range got {
if d.Type == TypeHoliday && d.Name == c.name {
n++
}
}
if n != c.count {
t.Errorf("%s 放假天数应为 %d,实际 %d", c.name, c.count, n)
}
if v, ok := byDate[c.start]; !ok || v.Type != TypeHoliday {
t.Errorf("%s 应为 %s 放假日,实际 %+v", c.start, c.name, v)
}
}
}
// TestFallbackAllYears 校验三年内置数据都能被索引到,且每年都有调休上班日。
func TestFallbackAllYears(t *testing.T) {
for _, y := range Years() {
list := builtinByYear(y)
if len(list) == 0 {
t.Errorf("%d 年内置数据为空", y)
continue
}
hasWork := false
for _, d := range list {
if d.Type == TypeWorkday {
hasWork = true
break
}
}
if !hasWork {
t.Errorf("%d 年缺少调休上班日数据", y)
}
}
}
// TestAnnotateWage 校验薪资倍数标注:春节前 3 天三薪,其余两薪。
func TestAnnotateWage(t *testing.T) {
list := []Day{
{Date: "2026-02-15", Name: "春节", Type: TypeHoliday},
{Date: "2026-02-16", Name: "春节", Type: TypeHoliday},
{Date: "2026-02-17", Name: "春节", Type: TypeHoliday},
{Date: "2026-02-18", Name: "春节", Type: TypeHoliday},
{Date: "2026-02-19", Name: "春节", Type: TypeHoliday},
}
got := annotateWage(list)
want := []int8{3, 3, 3, 2, 2}
for i, w := range want {
if got[i].Wage != w {
t.Errorf("%s 薪资倍数应为 %d,实际 %d", got[i].Date, w, got[i].Wage)
}
}
}
// TestExpandRange 校验日期区间展开。
func TestExpandRange(t *testing.T) {
got := expandRange("2026-02-15", "2026-02-23")
if len(got) != 9 {
t.Fatalf("2/15-2/23 应展开 9 天,实际 %d", len(got))
}
if got[0] != "2026-02-15" || got[8] != "2026-02-23" {
t.Errorf("区间首尾错误: %v", got)
}
// 单日
if one := expandRange("2026-01-01", ""); len(one) != 1 || one[0] != "2026-01-01" {
t.Errorf("单日区间展开错误: %v", one)
}
// 跨月
if cross := expandRange("2026-02-28", "2026-03-02"); len(cross) != 3 {
t.Errorf("跨月区间展开错误: %v", cross)
}
// 异常区间不 panic 且返回空
if bad := expandRange("2026-03-01", "2026-02-01"); len(bad) != 0 {
t.Errorf("结束早于开始应返回空,实际 %v", bad)
}
}
// TestDedupSort 校验同日去重时休假日优先于调休上班日。
func TestDedupSort(t *testing.T) {
list := []Day{
{Date: "2026-05-09", Name: "调休上班", Type: TypeWorkday},
{Date: "2026-05-01", Name: "劳动节", Type: TypeHoliday},
{Date: "2026-05-09", Name: "劳动节", Type: TypeHoliday},
}
got := dedupSort(list)
if len(got) != 2 {
t.Fatalf("去重后应为 2 条,实际 %d", len(got))
}
if got[0].Date != "2026-05-01" {
t.Errorf("排序错误,首条应为 05-01,实际 %s", got[0].Date)
}
if got[1].Type != TypeHoliday {
t.Errorf("05-09 同日冲突时应保留休假日,实际 %+v", got[1])
}
}
// TestFetchYearLive 联网校验真实数据源可用性(仅测"未收录年份"的联网分支)。
// 2024-2026 走内置数据不联网,因此这里用 2023 年验证外网拉取能力;
// 若外网不可用则跳过,不把 CI 绑死在第三方接口可用性上。
func TestFetchYearLive(t *testing.T) {
if testing.Short() {
t.Skip("跳过联网测试")
}
// 2023 未被内置数据收录,必须联网
list, src, err := FetchYear(2023)
if err != nil {
t.Skipf("外网不可用,跳过联网测试: %v", err)
}
if src != SourceHolidayCN {
t.Fatalf("2023 年应来自外网源,实际来源 %s", src)
}
if len(list) == 0 {
t.Fatal("2023 年数据为空")
}
t.Logf("2023 年数据来源: %s,共 %d 条", src, len(list))
}
// TestBuiltinTakesPriority 校验已收录年份走内置数据、完全不联网。
// 这是"日历翻月不卡顿"的核心保证:若哪天有人把网络查询挪到内置判断之前,
// 本测试会立即失败。
func TestBuiltinTakesPriority(t *testing.T) {
for _, y := range []int{2024, 2025, 2026} {
list, src, err := FetchYear(y)
if err != nil {
t.Fatalf("%d 年获取失败: %v", y, err)
}
if src != SourceBuiltin {
t.Errorf("%d 年应命中内置数据(零网络请求),实际来源 %s", y, src)
}
if len(list) == 0 {
t.Errorf("%d 年数据为空", y)
}
}
}
// TestFetchYearFastForBuiltin 量化验证内置路径的耗时:
// 已收录年份必须是纯内存操作(远低于 50ms),而跨境网络请求至少要 300ms。
func TestFetchYearFastForBuiltin(t *testing.T) {
// 预热,排除首次调用的初始化开销
_, _, _ = FetchYear(2026)
start := time.Now()
const rounds = 200
for i := 0; i < rounds; i++ {
if _, src, err := FetchYear(2026); err != nil || src != SourceBuiltin {
t.Fatalf("第 %d 次调用异常: src=%s err=%v", i, src, err)
}
}
elapsed := time.Since(start)
perCall := elapsed / rounds
if perCall > 50*time.Millisecond {
t.Errorf("已收录年份单次耗时应远低于 50ms(不应联网),实际 %v", perCall)
}
t.Logf("2026 年(内置)单次平均耗时: %v,%d 次共 %v", perCall, rounds, elapsed)
}