Ctrl K
ComunidadGuías…

model/online_history_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)))
	}
}