mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-11-04 00:40:20 +00:00
Add persisted online sessions, configurable retention and online history statistics with lifecycle, recovery, timezone and concurrency regression coverage. Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
221 lines
8.4 KiB
Go
221 lines
8.4 KiB
Go
package model
|
||
|
||
import (
|
||
"math/rand"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
var testZone = time.FixedZone("TEST", 8*3600)
|
||
|
||
func ts(y int, mo time.Month, d, h, mi, s int) time.Time {
|
||
return time.Date(y, mo, d, h, mi, s, 0, testZone)
|
||
}
|
||
|
||
func TestClipOnlineSessions(t *testing.T) {
|
||
windowStart := ts(2026, 10, 1, 0, 0, 0)
|
||
windowEnd := ts(2026, 10, 8, 0, 0, 0)
|
||
|
||
t.Run("session fully inside window", func(t *testing.T) {
|
||
sessions := []OnlineSession{
|
||
{ServerID: 1, StartedAt: ts(2026, 10, 2, 8, 0, 0), EndedAt: ts(2026, 10, 2, 9, 30, 0)},
|
||
}
|
||
periods := ClipOnlineSessions(sessions, windowStart, windowEnd)
|
||
require.Len(t, periods, 1)
|
||
require.Equal(t, ts(2026, 10, 2, 8, 0, 0), periods[0].Start)
|
||
require.Equal(t, ts(2026, 10, 2, 9, 30, 0), periods[0].End)
|
||
require.False(t, periods[0].Ongoing)
|
||
})
|
||
|
||
t.Run("session started before window is clipped", func(t *testing.T) {
|
||
sessions := []OnlineSession{
|
||
{ServerID: 1, StartedAt: ts(2026, 9, 28, 12, 0, 0), EndedAt: ts(2026, 10, 1, 5, 0, 0)},
|
||
}
|
||
periods := ClipOnlineSessions(sessions, windowStart, windowEnd)
|
||
require.Len(t, periods, 1)
|
||
require.Equal(t, windowStart, periods[0].Start)
|
||
require.Equal(t, ts(2026, 10, 1, 5, 0, 0), periods[0].End)
|
||
})
|
||
|
||
t.Run("ongoing session ends at window end", func(t *testing.T) {
|
||
sessions := []OnlineSession{
|
||
{ServerID: 1, StartedAt: ts(2026, 10, 7, 10, 0, 0), Ongoing: true},
|
||
}
|
||
periods := ClipOnlineSessions(sessions, windowStart, windowEnd)
|
||
require.Len(t, periods, 1)
|
||
require.Equal(t, windowEnd, periods[0].End)
|
||
require.True(t, periods[0].Ongoing)
|
||
})
|
||
|
||
t.Run("zero ended_at treated as ongoing", func(t *testing.T) {
|
||
sessions := []OnlineSession{
|
||
{ServerID: 1, StartedAt: ts(2026, 10, 7, 10, 0, 0)},
|
||
}
|
||
periods := ClipOnlineSessions(sessions, windowStart, windowEnd)
|
||
require.Len(t, periods, 1)
|
||
require.True(t, periods[0].Ongoing)
|
||
require.Equal(t, windowEnd, periods[0].End)
|
||
})
|
||
|
||
t.Run("session outside window yields no period", func(t *testing.T) {
|
||
sessions := []OnlineSession{
|
||
{ServerID: 1, StartedAt: ts(2026, 9, 1, 0, 0, 0), EndedAt: ts(2026, 9, 2, 0, 0, 0)},
|
||
}
|
||
require.Empty(t, ClipOnlineSessions(sessions, windowStart, windowEnd))
|
||
})
|
||
}
|
||
|
||
func TestDailyOnlineBreakdown(t *testing.T) {
|
||
loc := time.FixedZone("UTC+8", 8*3600)
|
||
windowStart := time.Date(2026, 10, 1, 10, 0, 0, 0, loc)
|
||
windowEnd := time.Date(2026, 10, 3, 14, 0, 0, 0, loc)
|
||
|
||
periods := []OnlinePeriod{
|
||
// 跨午夜:2 日 23:00 ~ 3 日 01:00
|
||
{Start: time.Date(2026, 10, 2, 23, 0, 0, 0, loc), End: time.Date(2026, 10, 3, 1, 0, 0, 0, loc)},
|
||
// 窗口前就开始的会话,裁剪后从窗口起点开始
|
||
{Start: windowStart, End: time.Date(2026, 10, 1, 12, 0, 0, 0, loc)},
|
||
}
|
||
|
||
daily := DailyOnlineBreakdown(periods, windowStart, windowEnd, loc)
|
||
require.Len(t, daily, 3)
|
||
require.Equal(t, "2026-10-01", daily[0].Date)
|
||
require.Equal(t, "2026-10-02", daily[1].Date)
|
||
require.Equal(t, "2026-10-03", daily[2].Date)
|
||
|
||
// 1 日:12:00-10:00 = 2h;2 日:23:00-24:00 = 1h;3 日:0:00-1:00 + 窗口外无 = 1h
|
||
require.Equal(t, uint64(7200), daily[0].OnlineSeconds)
|
||
require.Equal(t, uint64(3600), daily[1].OnlineSeconds)
|
||
require.Equal(t, uint64(3600), daily[2].OnlineSeconds)
|
||
|
||
// 每日合计即总时长(口径:每日累计原始时长后统一取整)
|
||
var sum uint64
|
||
for _, d := range daily {
|
||
sum += d.OnlineSeconds
|
||
}
|
||
require.Equal(t, uint64(7200+3600+3600), sum)
|
||
}
|
||
|
||
// 评审回归:取整必须按日累计原始时长后统一转换。逐段截断再累加会让亚秒
|
||
// 会话系统性偏小——同一天两个 1.5s 的会话应得 3s,而不是 1+1=2s
|
||
func TestDailyOnlineBreakdownSubSecond(t *testing.T) {
|
||
loc := testZone
|
||
windowStart := ts(2026, 10, 1, 0, 0, 0)
|
||
windowEnd := ts(2026, 10, 2, 0, 0, 0)
|
||
periods := []OnlinePeriod{
|
||
{Start: windowStart.Add(100 * time.Millisecond), End: windowStart.Add(1600 * time.Millisecond)},
|
||
{Start: windowStart.Add(2 * time.Second), End: windowStart.Add(3500 * time.Millisecond)},
|
||
}
|
||
daily := DailyOnlineBreakdown(periods, windowStart, windowEnd, loc)
|
||
require.Len(t, daily, 1)
|
||
require.Equal(t, uint64(3), daily[0].OnlineSeconds)
|
||
}
|
||
|
||
// 跨日时段按日界切分不损失精度:各日分别累计原始时长后各自取整
|
||
func TestDailyOnlineBreakdownCrossDayFraction(t *testing.T) {
|
||
loc := testZone
|
||
windowStart := ts(2026, 10, 1, 0, 0, 0)
|
||
windowEnd := ts(2026, 10, 3, 0, 0, 0)
|
||
periods := []OnlinePeriod{{
|
||
Start: time.Date(2026, 10, 1, 23, 59, 59, 250000000, loc),
|
||
End: time.Date(2026, 10, 2, 0, 0, 2, 750000000, loc),
|
||
}}
|
||
daily := DailyOnlineBreakdown(periods, windowStart, windowEnd, loc)
|
||
require.Len(t, daily, 2)
|
||
require.Equal(t, uint64(0), daily[0].OnlineSeconds) // 0.75s → 0
|
||
require.Equal(t, uint64(2), daily[1].OnlineSeconds) // 2.75s → 2
|
||
}
|
||
|
||
func TestDailyOnlineBreakdownZeroOnline(t *testing.T) {
|
||
loc := time.FixedZone("UTC+8", 8*3600)
|
||
windowStart := time.Date(2026, 10, 1, 0, 0, 0, 0, loc)
|
||
windowEnd := time.Date(2026, 10, 4, 0, 0, 0, 0, loc)
|
||
daily := DailyOnlineBreakdown(nil, windowStart, windowEnd, loc)
|
||
require.Len(t, daily, 3)
|
||
for _, d := range daily {
|
||
require.Zero(t, d.OnlineSeconds)
|
||
}
|
||
}
|
||
|
||
func TestOnlineStatsDaysBounds(t *testing.T) {
|
||
require.Equal(t, 7, DefaultOnlineStatsDays)
|
||
require.Equal(t, 7, MinOnlineStatsDays)
|
||
require.Equal(t, 3650, MaxOnlineStatsDays)
|
||
}
|
||
|
||
// F1 回归保护:指针推进实现必须与逐日朴素实现对任意不相交时段集合等价
|
||
// (单服务器会话互不重叠的约束一致),长窗口大量时段下不再随时段数平方放大。
|
||
func TestDailyOnlineBreakdownEquivalence(t *testing.T) {
|
||
loc := testZone
|
||
windowStart := ts(2026, 1, 1, 0, 0, 0)
|
||
windowEnd := windowStart.AddDate(0, 0, 90)
|
||
|
||
rng := rand.New(rand.NewSource(42))
|
||
var periods []OnlinePeriod
|
||
cursor := windowStart
|
||
for cursor.Before(windowEnd) {
|
||
// 起止带亚秒抖动:让逐段截断与按日累计两种口径可区分
|
||
start := cursor.Add(time.Duration(rng.Intn(4*3600))*time.Second + time.Duration(rng.Int63n(int64(time.Second))))
|
||
end := start.Add(time.Duration(1+rng.Intn(6*3600))*time.Second + time.Duration(rng.Int63n(int64(time.Second))))
|
||
if end.After(windowEnd) {
|
||
end = windowEnd
|
||
}
|
||
periods = append(periods, OnlinePeriod{Start: start, End: end})
|
||
cursor = end.Add(time.Duration(1+rng.Intn(3600)) * time.Second)
|
||
}
|
||
|
||
days := DailyOnlineBreakdown(periods, windowStart, windowEnd, loc)
|
||
|
||
// 参考实现与生产同一口径:每日累计原始时长、汇总时向下取整一次
|
||
reference := map[string]time.Duration{}
|
||
for _, p := range periods {
|
||
for d := windowStart; d.Before(windowEnd); d = d.AddDate(0, 0, 1) {
|
||
overlapStart := maxTime(p.Start, d)
|
||
overlapEnd := minTime(p.End, d.AddDate(0, 0, 1))
|
||
if overlapStart.Before(overlapEnd) {
|
||
reference[d.Format("2006-01-02")] += overlapEnd.Sub(overlapStart)
|
||
}
|
||
}
|
||
}
|
||
require.Len(t, days, 90)
|
||
for _, day := range days {
|
||
require.Equal(t, uint64(reference[day.Date].Seconds()), day.OnlineSeconds, "date %s", day.Date)
|
||
}
|
||
}
|
||
|
||
func TestDailyOnlineBreakdownMidnightDSTTransition(t *testing.T) {
|
||
loc, err := time.LoadLocation("America/Sao_Paulo")
|
||
require.NoError(t, err)
|
||
start := time.Date(2018, 11, 3, 0, 0, 0, 0, loc)
|
||
end := time.Date(2018, 11, 6, 0, 0, 0, 0, loc)
|
||
days := DailyOnlineBreakdown([]OnlinePeriod{{Start: start, End: end}}, start, end, loc)
|
||
require.Equal(t, []DailyOnlineStat{{Date: "2018-11-03", OnlineSeconds: 86400}, {Date: "2018-11-04", OnlineSeconds: 82800}, {Date: "2018-11-05", OnlineSeconds: 86400}}, days)
|
||
}
|
||
|
||
func TestDailyOnlineBreakdownRepeatedMidnight(t *testing.T) {
|
||
loc, err := time.LoadLocation("Asia/Amman")
|
||
require.NoError(t, err)
|
||
start := time.Date(2021, 10, 28, 21, 0, 0, 0, time.UTC)
|
||
end := time.Date(2021, 10, 29, 22, 0, 0, 0, time.UTC)
|
||
days := DailyOnlineBreakdown([]OnlinePeriod{{Start: start, End: end}}, start, end, loc)
|
||
require.Equal(t, []DailyOnlineStat{{Date: "2021-10-29", OnlineSeconds: 90000}}, days)
|
||
}
|
||
|
||
func TestOnlineHistoryWindowUsesDashboardCalendar(t *testing.T) {
|
||
loc, err := time.LoadLocation("America/New_York")
|
||
require.NoError(t, err)
|
||
for _, test := range []struct {
|
||
month time.Month
|
||
day, hours int
|
||
}{{time.March, 9, 23}, {time.November, 2, 25}} {
|
||
end := time.Date(2026, test.month, test.day, 0, 30, 0, 0, loc)
|
||
start := OnlineHistoryWindowStart(end.UTC(), 1, loc)
|
||
require.True(t, start.Equal(time.Date(2026, test.month, test.day-1, 0, 30, 0, 0, loc)))
|
||
require.Equal(t, time.Duration(test.hours)*time.Hour, end.Sub(start))
|
||
require.Len(t, DailyOnlineBreakdown(nil, start, end, loc), 2)
|
||
require.True(t, OnlineHistoryWindowStart(end.UTC(), 7, loc).Equal(end.In(loc).AddDate(0, 0, -7)))
|
||
}
|
||
}
|