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) }