更新日历功能
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user