Files
yunzerwebsiteallinone/go/pkg/holiday/holiday_test.go
T
2026-09-24 15:14:30 +08:00

230 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}