refactor: move some utility functions to goutils and update references

This commit is contained in:
yusing
2025-12-04 12:17:33 +08:00
parent fac3d67a51
commit 3b2ae5dbd6
27 changed files with 41 additions and 430 deletions

Submodule goutils updated: cfb8d74b17...dc10bf40f9

View File

@@ -14,7 +14,6 @@ import (
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/maxmind"
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
@@ -82,7 +81,7 @@ var ActiveConfig atomic.Pointer[Config]
const cacheTTL = 1 * time.Minute
func (c *checkCache) Expired() bool {
return c.created.Add(cacheTTL).Before(utils.TimeNow())
return c.created.Add(cacheTTL).Before(time.Now())
}
// TODO: add stats
@@ -180,7 +179,7 @@ func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
c.ipCache.Store(info.Str, &checkCache{
IPInfo: info,
allow: allow,
created: utils.TimeNow(),
created: time.Now(),
})
}

View File

@@ -6,8 +6,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/fs"
)
type ListFilesResponse struct {
@@ -35,7 +35,7 @@ func List(c *gin.Context) {
}
// config/
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
files, err := fs.ListFiles(common.ConfigBasePath, 0, true)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to list files"))
return
@@ -48,7 +48,7 @@ func List(c *gin.Context) {
}
// config/middlewares/
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
mids, err := fs.ListFiles(common.MiddlewareComposeBasePath, 0, true)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to list files"))
return

View File

@@ -15,7 +15,6 @@ import (
"github.com/go-acme/lego/v4/lego"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
)
@@ -96,7 +95,7 @@ func (cfg *Config) Validate() gperr.Error {
if cfg.Provider != ProviderCustom {
b.Add(ErrUnknownProvider.
Subject(cfg.Provider).
With(gperr.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
With(gperr.DoYouMeanField(cfg.Provider, Providers)))
}
} else {
provider, err := providerConstructor(cfg.Options)

View File

@@ -17,7 +17,6 @@ import (
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
)
@@ -224,7 +223,7 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
}
}
}
nearest := gperr.DoYouMean(utils.NearestField(c.Network, helper.NetworkSettings.Networks))
nearest := gperr.DoYouMeanField(c.Network, helper.NetworkSettings.Networks)
addError(c, fmt.Errorf("network %q not found, %w", c.Network, nearest))
return
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
nettypes "github.com/yusing/godoxy/internal/net/types"
"github.com/yusing/godoxy/internal/utils/pool"
"github.com/yusing/goutils/pool"
)
type route interface {

View File

@@ -9,7 +9,7 @@ import (
"time"
. "github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/goutils/mockable"
"github.com/yusing/goutils/task"
expect "github.com/yusing/goutils/testing"
)
@@ -57,7 +57,7 @@ func fmtLog(cfg *RequestLoggerConfig) (ts string, line string) {
t := time.Now()
logger := NewMockAccessLogger(testTask, cfg)
utils.MockTimeNow(t)
mockable.MockTimeNow(t)
buf = logger.(RequestFormatter).AppendRequestLog(buf, req, resp)
return t.Format(LogTimeFormat), string(buf)
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/rs/zerolog"
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/goutils/mockable"
)
type (
@@ -67,7 +67,7 @@ func (f *CommonFormatter) AppendRequestLog(line []byte, req *http.Request, res *
line = append(line, clientIP(req)...)
line = append(line, " - - ["...)
line = utils.TimeNow().AppendFormat(line, LogTimeFormat)
line = mockable.TimeNow().AppendFormat(line, LogTimeFormat)
line = append(line, `] "`...)
line = append(line, req.Method...)
@@ -103,7 +103,7 @@ func (f *JSONFormatter) AppendRequestLog(line []byte, req *http.Request, res *ht
writer := bytes.NewBuffer(line)
logger := zerolog.New(writer)
event := logger.Info().
Str("time", utils.TimeNow().Format(LogTimeFormat)).
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
Str("ip", clientIP(req)).
Str("method", req.Method).
Str("scheme", scheme(req)).
@@ -136,7 +136,7 @@ func (f ACLLogFormatter) AppendACLLog(line []byte, info *maxmind.IPInfo, blocked
writer := bytes.NewBuffer(line)
logger := zerolog.New(writer)
event := logger.Info().
Str("time", utils.TimeNow().Format(LogTimeFormat)).
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
Str("ip", info.Str)
if blocked {
event.Str("action", "block")

View File

@@ -8,8 +8,8 @@ import (
"time"
"github.com/rs/zerolog"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/mockable"
strutils "github.com/yusing/goutils/strings"
)
@@ -81,14 +81,14 @@ func rotateLogFile(file supportRotate, config *Retention, result *RotateResult)
func rotateLogFileByPolicy(file supportRotate, config *Retention, result *RotateResult) (rotated bool, err error) {
var shouldStop func() bool
t := utils.TimeNow()
t := mockable.TimeNow()
switch {
case config.Last > 0:
shouldStop = func() bool { return result.NumLinesKeep-result.NumLinesInvalid == int(config.Last) }
// not needed to parse time for last N lines
case config.Days > 0:
cutoff := utils.TimeNow().AddDate(0, 0, -int(config.Days)+1)
cutoff := mockable.TimeNow().AddDate(0, 0, -int(config.Days)+1)
shouldStop = func() bool { return t.Before(cutoff) }
default:
return false, nil // should not happen

View File

@@ -7,7 +7,7 @@ import (
"time"
. "github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/goutils/mockable"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
expect "github.com/yusing/goutils/testing"
@@ -56,7 +56,7 @@ func TestRotateKeepLast(t *testing.T) {
for _, format := range ReqLoggerFormats {
t.Run(string(format)+" keep last", func(t *testing.T) {
file := NewMockFile(true)
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
Format: format,
})
@@ -93,7 +93,7 @@ func TestRotateKeepLast(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 10
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -105,7 +105,7 @@ func TestRotateKeepLast(t *testing.T) {
expect.Equal(t, retention.KeepSize, 0)
logger.Config().Retention = retention
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
var result RotateResult
rotated, err := logger.(AccessLogRotater).Rotate(&result)
expect.NoError(t, err)
@@ -139,7 +139,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 10
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -151,7 +151,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Equal(t, retention.Last, 0)
logger.Config().Retention = retention
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
var result RotateResult
rotated, err := logger.(AccessLogRotater).Rotate(&result)
expect.NoError(t, err)
@@ -171,7 +171,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 100
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -183,7 +183,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Equal(t, retention.Last, 0)
logger.Config().Retention = retention
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
var result RotateResult
rotated, err := logger.(AccessLogRotater).Rotate(&result)
expect.NoError(t, err)
@@ -205,7 +205,7 @@ func TestRotateSkipInvalidTime(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 10
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
logger.Flush()
@@ -248,7 +248,7 @@ func BenchmarkRotate(b *testing.B) {
Format: FormatJSON,
})
for i := range 100 {
utils.MockTimeNow(testTime.AddDate(0, 0, -100+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -100+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -282,7 +282,7 @@ func BenchmarkRotateWithInvalidTime(b *testing.B) {
Format: FormatJSON,
})
for i := range 10000 {
utils.MockTimeNow(testTime.AddDate(0, 0, -10000+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -10000+i+1))
logger.Log(req, resp)
if i%10 == 0 {
_, _ = file.Write([]byte("invalid time\n"))

View File

@@ -10,8 +10,8 @@ import (
"github.com/rs/zerolog/log"
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/utils/pool"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/pool"
"github.com/yusing/goutils/task"
"golang.org/x/sync/errgroup"
)

View File

@@ -8,7 +8,6 @@ import (
_ "embed"
"github.com/yusing/godoxy/internal/jsonstore"
"github.com/yusing/godoxy/internal/utils"
)
type CaptchaSession struct {
@@ -22,7 +21,7 @@ var CaptchaSessions = jsonstore.Store[*CaptchaSession]("captcha_sessions")
func newCaptchaSession(p Provider) *CaptchaSession {
buf := make([]byte, 32)
_, _ = rand.Read(buf)
now := utils.TimeNow()
now := time.Now()
return &CaptchaSession{
ID: hex.EncodeToString(buf),
Expiry: now.Add(p.SessionExpiry()),
@@ -30,5 +29,5 @@ func newCaptchaSession(p Provider) *CaptchaSession {
}
func (s *CaptchaSession) expired() bool {
return utils.TimeNow().After(s.Expiry)
return time.Now().After(s.Expiry)
}

View File

@@ -9,10 +9,10 @@ import (
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/godoxy/internal/watcher"
"github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/fs"
"github.com/yusing/goutils/task"
)
@@ -46,7 +46,7 @@ func GetErrorPageByStatus(statusCode int) (content []byte, ok bool) {
}
func loadContent() {
files, err := utils.ListFiles(errPagesBasePath, 0)
files, err := fs.ListFiles(errPagesBasePath, 0)
if err != nil {
log.Err(err).Msg("failed to list error page resources")
return

View File

@@ -7,8 +7,8 @@ import (
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
fsutils "github.com/yusing/goutils/fs"
strutils "github.com/yusing/goutils/strings"
)
@@ -52,7 +52,7 @@ func Get(name string) (*Middleware, Error) {
if !ok {
return nil, ErrUnknownMiddleware.
Subject(name).
With(gperr.DoYouMean(utils.NearestField(name, allMiddlewares)))
With(gperr.DoYouMeanField(name, allMiddlewares))
}
return middleware, nil
}
@@ -63,7 +63,7 @@ func All() map[string]*Middleware {
func LoadComposeFiles() {
errs := gperr.NewBuilder("middleware compile errors")
middlewareDefs, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0)
middlewareDefs, err := fsutils.ListFiles(common.MiddlewareComposeBasePath, 0)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return

View File

@@ -4,11 +4,9 @@ import (
urlPkg "net/url"
"github.com/bytedance/sonic"
"github.com/yusing/godoxy/internal/utils"
)
type URL struct {
_ utils.NoCopy
urlPkg.URL
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/bytedance/sonic"
"github.com/luthermonson/go-proxmox"
"github.com/yusing/godoxy/internal/utils/pool"
"github.com/yusing/goutils/pool"
)
type Node struct {

View File

@@ -2,7 +2,7 @@ package routes
import (
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/utils/pool"
"github.com/yusing/goutils/pool"
)
var (

View File

@@ -13,7 +13,6 @@ import (
"github.com/go-playground/validator/v10"
"github.com/goccy/go-yaml"
"github.com/puzpuzpuz/xsync/v4"
"github.com/yusing/godoxy/internal/utils"
gi "github.com/yusing/gointernals"
"github.com/yusing/goutils/env"
gperr "github.com/yusing/goutils/errs"
@@ -300,7 +299,7 @@ func mapUnmarshalValidate(src SerializedObject, dstV reflect.Value, checkValidat
errs.Add(err.Subject(k))
}
} else {
errs.Add(ErrUnknownField.Subject(k).With(gperr.DoYouMean(utils.NearestField(k, info.fieldNames))))
errs.Add(ErrUnknownField.Subject(k).With(gperr.DoYouMeanField(k, info.fieldNames)))
}
}
if info.hasValidateTag && checkValidateTag {

View File

@@ -5,7 +5,6 @@ import (
"github.com/moby/moby/api/types/container"
"github.com/yusing/ds/ordered"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
)
@@ -14,8 +13,6 @@ type (
PortMapping = map[int]container.PortSummary
Container struct {
_ utils.NoCopy
DockerHost string `json:"docker_host"`
Image *ContainerImage `json:"image"`
ContainerName string `json:"container_name"`

View File

@@ -7,9 +7,9 @@ import (
"github.com/yusing/godoxy/internal/homepage"
nettypes "github.com/yusing/godoxy/internal/net/types"
provider "github.com/yusing/godoxy/internal/route/provider/types"
"github.com/yusing/godoxy/internal/utils/pool"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/http/reverseproxy"
"github.com/yusing/goutils/pool"
"github.com/yusing/goutils/task"
)

View File

@@ -1,36 +0,0 @@
package utils
import (
"fmt"
"os"
"path"
)
// Recursively lists all files in a directory until `maxDepth` is reached
// Returns a slice of file paths relative to `dir`.
func ListFiles(dir string, maxDepth int, hideHidden ...bool) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("error listing directory %s: %w", dir, err)
}
hideHiddenFiles := len(hideHidden) > 0 && hideHidden[0]
files := make([]string, 0)
for _, entry := range entries {
if hideHiddenFiles && entry.Name()[0] == '.' {
continue
}
if entry.IsDir() {
if maxDepth <= 0 {
continue
}
subEntries, err := ListFiles(path.Join(dir, entry.Name()), maxDepth-1)
if err != nil {
return nil, err
}
files = append(files, subEntries...)
} else {
files = append(files, path.Join(dir, entry.Name()))
}
}
return files, nil
}

View File

@@ -1,50 +0,0 @@
package utils
import (
"reflect"
strutils "github.com/yusing/goutils/strings"
)
func NearestField(input string, s any) string {
minDistance := -1
nearestField := ""
var fields []string
switch s := s.(type) {
case []string:
fields = s
default:
t := reflect.TypeOf(s)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
switch t.Kind() {
case reflect.Struct:
fields = make([]string, 0)
for i := range t.NumField() {
jsonTag, ok := t.Field(i).Tag.Lookup("json")
if ok {
fields = append(fields, jsonTag)
} else {
fields = append(fields, t.Field(i).Name)
}
}
case reflect.Map:
keys := reflect.ValueOf(s).MapKeys()
fields = make([]string, len(keys))
for i, key := range keys {
fields[i] = key.String()
}
default:
panic("NearestField unsupported type: " + t.String())
}
}
for _, field := range fields {
distance := strutils.LevenshteinDistance(input, field)
if minDistance == -1 || distance < minDistance {
minDistance = distance
nearestField = field
}
}
return nearestField
}

View File

@@ -1,123 +0,0 @@
package pool
import (
"sort"
"sync/atomic"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
)
type (
Pool[T Object] struct {
m *xsync.Map[string, T]
name string
disableLog atomic.Bool
}
// Preferable allows an object to express deterministic replacement preference
// when multiple objects with the same key are added to the pool.
// If new.PreferOver(old) returns true, the new object replaces the old one.
Preferable interface {
PreferOver(other any) bool
}
Object interface {
Key() string
Name() string
}
ObjectWithDisplayName interface {
Object
DisplayName() string
}
)
func New[T Object](name string) Pool[T] {
return Pool[T]{m: xsync.NewMap[string, T](), name: name}
}
func (p *Pool[T]) ToggleLog(v bool) {
p.disableLog.Store(v)
}
func (p *Pool[T]) Name() string {
return p.name
}
func (p *Pool[T]) Add(obj T) {
p.AddKey(obj.Key(), obj)
}
func (p *Pool[T]) AddKey(key string, obj T) {
if cur, exists := p.m.Load(key); exists {
if newPref, ok := any(obj).(Preferable); ok {
if !newPref.PreferOver(cur) {
// keep existing
return
}
}
}
p.checkExists(key)
p.m.Store(key, obj)
p.logAction("added", obj)
}
func (p *Pool[T]) AddIfNotExists(obj T) (actual T, added bool) {
actual, loaded := p.m.LoadOrStore(obj.Key(), obj)
if !loaded {
p.logAction("added", obj)
}
return actual, !loaded
}
func (p *Pool[T]) Del(obj T) {
p.m.Delete(obj.Key())
p.logAction("removed", obj)
}
func (p *Pool[T]) DelKey(key string) {
if v, exists := p.m.LoadAndDelete(key); exists {
p.logAction("removed", v)
}
}
func (p *Pool[T]) Get(key string) (T, bool) {
return p.m.Load(key)
}
func (p *Pool[T]) Size() int {
return p.m.Size()
}
func (p *Pool[T]) Clear() {
p.m.Clear()
}
func (p *Pool[T]) Iter(fn func(k string, v T) bool) {
p.m.Range(fn)
}
func (p *Pool[T]) Slice() []T {
slice := make([]T, 0, p.m.Size())
for _, v := range p.m.Range {
slice = append(slice, v)
}
sort.Slice(slice, func(i, j int) bool {
return slice[i].Name() < slice[j].Name()
})
return slice
}
func (p *Pool[T]) logAction(action string, obj T) {
if p.disableLog.Load() {
return
}
if withName, ok := any(obj).(ObjectWithDisplayName); ok {
disp, name := withName.DisplayName(), withName.Name()
if disp != name {
log.Info().Msgf("%s: %s %s (%s)", p.name, action, disp, name)
} else {
log.Info().Msgf("%s: %s %s", p.name, action, name)
}
} else {
log.Info().Msgf("%s: %s %s", p.name, action, obj.Name())
}
}

View File

@@ -1,15 +0,0 @@
//go:build debug
package pool
import (
"runtime/debug"
"github.com/rs/zerolog/log"
)
func (p Pool[T]) checkExists(key string) {
if _, ok := p.m.Load(key); ok {
log.Warn().Msgf("%s: key %s already exists\nstacktrace: %s", p.name, key, string(debug.Stack()))
}
}

View File

@@ -1,7 +0,0 @@
//go:build !debug
package pool
func (p Pool[T]) checkExists(key string) {
// no-op in production
}

View File

@@ -1,44 +0,0 @@
package utils
import (
"time"
"go.uber.org/atomic"
)
var (
TimeNow = DefaultTimeNow
shouldCallTimeNow atomic.Bool
timeNowTicker = time.NewTicker(shouldCallTimeNowInterval)
lastTimeNow = atomic.NewTime(time.Now())
)
const shouldCallTimeNowInterval = 100 * time.Millisecond
func MockTimeNow(t time.Time) {
TimeNow = func() time.Time {
return t
}
}
// DefaultTimeNow is a time.Now wrapper that reduces the number of calls to time.Now
// by caching the result and only allow calling time.Now when the ticker fires.
//
// Returned value may have +-100ms error.
func DefaultTimeNow() time.Time {
swapped := shouldCallTimeNow.CompareAndSwap(false, true)
if swapped { // first call
now := time.Now()
lastTimeNow.Store(now)
return now
}
return lastTimeNow.Load()
}
func init() {
go func() {
for range timeNowTicker.C {
shouldCallTimeNow.Store(true)
}
}()
}

View File

@@ -1,104 +0,0 @@
package utils
import (
"testing"
"time"
)
var sink time.Time
func BenchmarkTimeNow(b *testing.B) {
b.Run("default", func(b *testing.B) {
for b.Loop() {
sink = time.Now()
}
})
b.Run("reduced_call", func(b *testing.B) {
for b.Loop() {
sink = DefaultTimeNow()
}
})
}
func TestDefaultTimeNow(t *testing.T) {
// Get initial time
t1 := DefaultTimeNow()
// Second call should return the same time without calling time.Now
t2 := DefaultTimeNow()
if !t1.Equal(t2) {
t.Errorf("Expected t1 == t2, got t1 = %v, t2 = %v", t1, t2)
}
// Set shouldCallTimeNow to true
shouldCallTimeNow.Store(true)
// This should update the lastTimeNow
t3 := DefaultTimeNow()
// The time should have changed
if t2.Equal(t3) {
t.Errorf("Expected t2 != t3, got t2 = %v, t3 = %v", t2, t3)
}
// Fourth call should return the same time as third call
t4 := DefaultTimeNow()
if !t3.Equal(t4) {
t.Errorf("Expected t3 == t4, got t3 = %v, t4 = %v", t3, t4)
}
}
func TestMockTimeNow(t *testing.T) {
// Save the original TimeNow function to restore later
originalTimeNow := TimeNow
defer func() {
TimeNow = originalTimeNow
}()
// Create a fixed time
fixedTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
// Mock the time
MockTimeNow(fixedTime)
// TimeNow should return the fixed time
result := TimeNow()
if !result.Equal(fixedTime) {
t.Errorf("Expected %v, got %v", fixedTime, result)
}
}
func TestTimeNowTicker(t *testing.T) {
// This test verifies that the ticker properly updates shouldCallTimeNow
// Reset the flag
shouldCallTimeNow.Store(false)
// Wait for the ticker to tick (slightly more than the interval)
time.Sleep(shouldCallTimeNowInterval + 10*time.Millisecond)
// The ticker should have set shouldCallTimeNow to true
if !shouldCallTimeNow.Load() {
t.Error("Expected shouldCallTimeNow to be true after ticker interval")
}
// Call DefaultTimeNow which should reset the flag
DefaultTimeNow()
// Check that the flag is reset
if shouldCallTimeNow.Load() {
t.Error("Expected shouldCallTimeNow to be false after calling DefaultTimeNow")
}
}
/*
BenchmarkTimeNow
BenchmarkTimeNow/default
BenchmarkTimeNow/default-20 48158628 24.86 ns/op 0 B/op 0 allocs/op
BenchmarkTimeNow/reduced_call
BenchmarkTimeNow/reduced_call-20 1000000000 1.000 ns/op 0 B/op 0 allocs/op
*/