Adguardhome/internal/stats/stats_internal_test.go
Fedor Setrakov b7c97190c6 Pull request 2568: AGDNS-3588-stats-api-intervals
Squashed commit of the following:

commit 377472005a80597eb8d226598825d5386a9d4557
Merge: fba031ac0 e87f57c1c
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Mon Feb 2 12:44:50 2026 +0300

    Merge branch 'master' into AGDNS-3588-stats-api-intervals

commit fba031ac02091e87007cb325f3b57c421f0d0a08
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Fri Jan 30 17:43:08 2026 +0300

    stats: imp tests

commit e640d7d5b3528f18a7ca5481c07bb4bae9c37836
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Thu Jan 29 17:33:27 2026 +0300

    all: revert upd go

commit 949dfc2a556c159a17699cad841957f873d539be
Merge: b6eb80e8a 8f55534e5
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Thu Jan 29 17:26:40 2026 +0300

    Merge branch 'master' into AGDNS-3588-stats-api-intervals

commit b6eb80e8a9b6ca30a8b2acce8851d7264d1744ee
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Thu Jan 29 11:36:10 2026 +0300

    all: fix race, imp validation, upd go

commit 5aba8e8f73c5bed08bade9984b81b0e12d1b526e
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Wed Jan 28 18:19:17 2026 +0300

    all: impl stats api intervals
2026-02-02 10:35:27 +00:00

191 lines
4.1 KiB
Go

package stats
import (
"cmp"
"fmt"
"path/filepath"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/agh"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testLogger is the common logger for tests.
var testLogger = slogutil.NewDiscardLogger()
// newTestStatsCtx returns StatsCtx initialised with given values. All empty
// values from c will be replaced with defaults.
func newTestStatsCtx(tb testing.TB, c Config) (s *StatsCtx) {
c.Logger = cmp.Or(c.Logger, testLogger)
c.ConfigModifier = cmp.Or[agh.ConfigModifier](c.ConfigModifier, agh.EmptyConfigModifier{})
c.HTTPReg = cmp.Or[aghhttp.Registrar](c.HTTPReg, aghhttp.EmptyRegistrar{})
c.Filename = cmp.Or(c.Filename, filepath.Join(tb.TempDir(), "./stats.db"))
c.Limit = cmp.Or(c.Limit, timeutil.Day)
if c.ShouldCountClient == nil {
c.ShouldCountClient = func([]string) bool { return true }
}
if c.UnitID == nil {
c.UnitID = newUnitID
}
var err error
s, err = New(c)
require.NoError(tb, err)
return s
}
func TestStats_races(t *testing.T) {
var r uint32
idGen := func() (id uint32) { return atomic.LoadUint32(&r) }
s := newTestStatsCtx(t, Config{
UnitID: idGen,
Enabled: true,
})
s.Start()
startTime := time.Now()
testutil.CleanupAndRequireSuccess(t, s.Close)
writeFunc := func(start, fin *sync.WaitGroup, waitCh <-chan unit, i int) {
e := &Entry{
Domain: fmt.Sprintf("example-%d.org", i),
Client: fmt.Sprintf("client_%d", i),
Result: Result(i)%(resultLast-1) + 1,
ProcessingTime: time.Since(startTime),
}
start.Done()
defer fin.Done()
<-waitCh
s.Update(e)
}
readFunc := func(start, fin *sync.WaitGroup, waitCh <-chan unit) {
start.Done()
defer fin.Done()
<-waitCh
_, _ = s.getData(24)
}
const (
roundsNum = 3
writersNum = 10
readersNum = 5
)
for round := range roundsNum {
atomic.StoreUint32(&r, uint32(round))
startWG, finWG := &sync.WaitGroup{}, &sync.WaitGroup{}
waitCh := make(chan unit)
for i := range writersNum {
startWG.Add(1)
finWG.Add(1)
go writeFunc(startWG, finWG, waitCh, i)
}
for range readersNum {
startWG.Add(1)
finWG.Add(1)
go readFunc(startWG, finWG, waitCh)
}
startWG.Wait()
close(waitCh)
finWG.Wait()
}
}
func TestStatsCtx_FillCollectedStats_daily(t *testing.T) {
const (
daysCount = 10
timeUnits = "days"
)
s := newTestStatsCtx(t, Config{
Limit: time.Hour,
Enabled: true,
})
testutil.CleanupAndRequireSuccess(t, s.Close)
sum := make([][]uint64, resultLast)
sum[RFiltered] = make([]uint64, daysCount)
sum[RSafeBrowsing] = make([]uint64, daysCount)
sum[RParental] = make([]uint64, daysCount)
total := make([]uint64, daysCount)
dailyData := []*unitDB{}
for i := range daysCount * 24 {
n := uint64(i)
nResult := make([]uint64, resultLast)
nResult[RFiltered] = n
nResult[RSafeBrowsing] = n
nResult[RParental] = n
day := i / 24
sum[RFiltered][day] += n
sum[RSafeBrowsing][day] += n
sum[RParental][day] += n
t := n * 3
total[day] += t
dailyData = append(dailyData, &unitDB{
NTotal: t,
NResult: nResult,
})
}
data := &StatsResp{}
// In this way we will not skip first hours.
curID := uint32(daysCount * 24)
s.fillCollectedStats(data, dailyData, curID)
assert.Equal(t, timeUnits, data.TimeUnits)
assert.Equal(t, sum[RFiltered], data.BlockedFiltering)
assert.Equal(t, sum[RSafeBrowsing], data.ReplacedSafebrowsing)
assert.Equal(t, sum[RParental], data.ReplacedParental)
assert.Equal(t, total, data.DNSQueries)
}
func TestStatsCtx_DataFromUnits_month(t *testing.T) {
const hoursInMonth = 720
s := newTestStatsCtx(t, Config{
Limit: time.Hour,
Enabled: true,
})
testutil.CleanupAndRequireSuccess(t, s.Close)
units, curID := s.loadUnits(hoursInMonth)
require.Len(t, units, hoursInMonth)
var h uint32
for h = 1; h <= hoursInMonth; h++ {
data := s.dataFromUnits(units[:h], curID)
require.NotNil(t, data)
}
}