ã¯ããã«
ããã«ã¡ã¯ãnewmoã§ã½ããã¦ã§ã¢ã¨ã³ã¸ãã¢ããã£ã¦ãã@tenntennã§ãã newmoã«ã¯2024å¹´8æã«å ¥ç¤¾ãã¾ããããã®è¨äºãæ¸ãã¦ããã®ã¯2024å¹´9æãªã®ã§ãå ¥ç¤¾ãã¦ã ããã1ã¶æã¡ãã£ã¨ãçµéããã¨ããã§ãã ãªããçè ãå ¥ç¤¾ããçµç·¯ãªã©ã¯æ¬¡ã®è¨äºãèªãã§ãã ããã
å ¥ç¤¾ããå½åãnewmoã®ããã¯ã¨ã³ãã³ã¼ãã®ã³ã¼ããçºãã¦ããã¨ã次ã®ããã«å®£è¨ãããé¢æ°ãè¦ã¤ãã¾ããã
func Now(_ context.Context) time.Time { return time.Now().In(time.UTC) }
åã«time.Now
é¢æ°ãå¼ã³åºãã¦ãLocation
ãUTC
ã«è¨å®ãã¦ããã ãã§ãã
ããããå¼æ°ã¯ãã©ã³ã¯èå¥åã«ãªã£ã¦ããã®ã§ä½¿ç¨ãã¦ãã¾ããã
ããããçè
ã¯ãããè¦ã¦ãããã¯å¾ã
ã®ãã¨ãèãã¦ãããªã¨æå¿ãã¾ããã
ã¡ãªã¿ã«å¤æ°ã¨éã£ã¦é¢æ°å
ã§ä½¿ç¨ãã¦ãªãã¦ãã³ã³ãã¤ã«ã¨ã©ã¼ã«ã¯ãªããªããããã©ã³ã¯èå¥åï¼_
ï¼ã«ããå¿
è¦ã¯ããã¾ããã
ããã«ãé¢ããããããã¦ãã©ã³ã¯èå¥åã«ãã¦ããã®ã¯ãä»ã¯ä½¿ã£ã¦ãªããã©å°æ¥ã¯ä½¿ãã¾ããã¨æ°æã¡ãæ示ãã¦ããããã«è¦ãã¾ããã
ï¼æå³ã¯ç¤¾å
ã§ç¢ºèªãã¦ãªãã®ã§ãèãããããããã¾ãããï¼
ã¡ãªã¿ã«ãGoã®è¨èªä»æ§ã§ã¯æ¬¡ã®ããã«ãã©ã³ã¯èå¥åãã使ç¨ãããååã ãã§ãåé¡ããã¾ããã ãã ãããã®å½¢ãåãå ´åã¯ããã¹ã¦ã®å¼æ°ã®èå¥åãçç¥ããå¿ è¦ãããã¾ãã
func Now(context.Context) time.Time { return time.Now().In(time.UTC) }
ãã¦ããã®å¼æ°ã®ã³ã³ããã¹ãã¯ä½ã®ããã«ããã®ã§ããããï¼ å®ã¯ãã®ã³ã³ããã¹ãã¯ãã¹ãã®éã«æå»ãã³ã³ããã¼ã«ããããã«ããã¾ãã ç¹ã«æå»ãç¹å®ã®æéã§åºå®ããããã«ä½¿ç¨ãããã¨ãæ³å®ãã¦ãã¾ãã
ãã®å¾ããããã¯ãéçºãã¦ããä¸ã§æå»ãåºå®ãã¦ãã¹ããããããªããçè ãã³ã³ããã¹ãã使ã£ã¦æå»ãåºå®ã§ããããã«å¤æ´ãã¾ããã ã¾ããæ¬ç¨¿ãæ¸ãã«ããã£ã¦ãã©ã¤ãã©ãªã¨ãã¦åãåºãOSSã¨ãã¦å ¬éãã¦ããã¾ãã
ããã§ãæ¬ç¨¿ã§ã¯ã³ã³ããã¹ãã使ã£ã¦æå»ãåºå®ããæ¹æ³ã¨ãã®æ¹æ³ã§å®è£ ãããã©ã¤ãã©ãªã«ã¤ãã¦ç´¹ä»ãã¾ãã
ã³ã³ããã¹ãã使ã£ã¦æå»ãåºå®ãã
ãã¹ãã®ããã«æå»ãåºå®ããæ¹æ³ã¯ããã¤ãåå¨ãã¾ãã ãã¨ãã°ãçè ãæ¸ããè¨äºã«ã¯æ¬¡ã®æ¹æ³ãæãããã¦ãã¾ããã
- å¼æ°ã«ç¾å¨æå»ã渡ã
- ããã±ã¼ã¸å¤æ°ããã£ã¼ã«ããªã©ã«ç¾å¨æå»ãè¿ãé¢æ°ãã¤ã³ã¿ãã§ã¼ã¹ãè¨å®ãã
context.WithValue
é¢æ°ã§ã³ã³ããã¹ãã«ç¾å¨æå»ãè¨ãã
スレッドセーフなテスト用の時間を固定するライブラリを作った - tenntenn.dev
ãã®è¨äºã§ã¯ããã«çè
ãéçºããtesttimeã¨ããã©ã¤ãã©ãªãç´¹ä»ããã¦ãã¾ããã
testtimeã¯ä¾¿å©ã§ãããlinknameã-overlay
ãã©ã°ãªã©ãè¥å¹²"ããã¡ã"ãªæ©è½ã使ç¨ãã¦ãã¾ããï¼è©³ç´°ã¯testimeã®è¨äºãã覧ãã ããï¼ã
ãã¹ãã®ã¨ãã ãã¨ã¯è¨ãã©ããã§ãããªã"é常ã®"ããæ¹ã§è§£æ±ºãããã¨ããã§ãã ãã¯ããå®å ¨ã«æå»ãåºå®ããã«ã¯ãããã¸ã§ã¯ãã®åæãããã¹ãã§æå»ãåºå®ããããªãã¨ããæ¥ããã¨ãèæ ®ãã¦ããæ¹ãè¯ãã§ãããã
åé ã«ç´¹ä»ããNow
é¢æ°ã¯ãã³ã³ããã¹ããå¼æ°ã«åã£ã¦ãããããå¾ããå¦ä½æ§ã«ãå®è£
ãæ¡å
ã§ããããã«ãªã£ã¦ãã¾ãã
ãã¨ãã°ã次ã®ãããªWithFixedNow
é¢æ°ãä½æããã³ã³ããã¹ãã«ç¹å®ã®æå»ãç´ã¥ãããã¨ãã§ãã¾ãã
func Now(ctx context.Context) time.Time { if testing.Testing() { return nowForTest(ctx) } return defaultNow(ctx) } func defaultNow(_ context.Context) time.Time { return time.Now().In(time.UTC) } func nowForTest(ctx context.Context) time.Time { now, ok := nowFromContext(ctx) if ok { return now } return defaultNow(ctx) } type ctxkey struct{} func WithFixedNow(t *testing.T, ctx context.Context, tm time.Time) context.Context { t.Helper() return context.WithValue(ctx, ctxkey{}, tm) } func nowFromContext(ctx context.Context) (time.Time, bool) { tm, ok := ctx.Value(ctxkey{}).(time.Time) return tm, ok }
WithFixedNow
ã®ç¬¬1å¼æ°ã«*testing.T
åãæå®ããå¿
è¦ãããã®ã¯ããã¹ã以å¤ã§å¼ã°ãããã¨ãé²ãããã§ãã
ã¾ããNow
é¢æ°ã¯testing.Testing
é¢æ°ãtrue
ãè¿ãå ´åã®ã¿ãã¤ã¾ããã¹ãã®æã ãã³ã³ããã¹ãããæå»ãåå¾ããããã«ãã¦ãã¾ãã
ãã¹ãã®æã ãæåãå¤ãã
åè¿°ã®æ¹æ³ã§ã¯ããã¹ãã®ã¨ãã«ã ãæå»ãåºå®ãããã¨ãã§ãã¾ãããNow
é¢æ°ã宣è¨ããããã±ã¼ã¸ãtesting
ããã±ã¼ã¸ãã¤ã³ãã¼ãããå¿
è¦ãããã¾ãã
ããã§ã次ã«ãã¹ã以å¤ã§ã¯testing
ããã±ã¼ã¸ãã¤ã³ãã¼ãããã«ããã¹ãã®æã ãæåãå¤ããæ¹æ³ãèãã¦ã¿ã¾ãããã
次ã®ããã«ãNow
é¢æ°ã宣è¨ãã¦ããããã±ã¼ã¸ãctxtime
ã¨åä»ãããã¹ãã§ä½¿ç¨ããWithFixedNow
é¢æ°ãªã©ã¯ããµãããã±ã¼ã¸ã®ctxtimetest
ããã±ã¼ã¸ã«ç§»åããã¾ãã
ã¾ããctxtime
ããã±ã¼ã¸ãtesting
ããã±ã¼ã¸ã«ä¾åããªãããã«ãinternal
ããã±ã¼ã¸ãç¨æãã¾ãã
ctxtime âââ ctxtime.go âââ ctxtimetest â  âââ ctxtimetest.go âââ internal âââ now.go
Goã«ããã¦ãinternal
ããã±ã¼ã¸ã¯ç¹å¥ãªããã±ã¼ã¸ã§ãã
internal
ã¨ããååã®ãã£ã¬ã¯ããªä»¥ä¸ã«é
ç½®ããã½ã¼ã¹ã³ã¼ããããã±ã¼ã¸ã¯ããã®internal
ãã£ã¬ã¯ããªãåå¨ãããã£ã¬ã¯ããªä»¥ä¸ã§ããåç
§ã§ãã¾ããã
詳細ã¯çè
ãåè·æ代ã«æ¸ããè¨äºãåç
§ãã¦ãã ããã
ããã§ã¯ãé
ç½®ãç´ããåãã¡ã¤ã«ã®ä¸èº«ãè¦ã¦ããã¾ãããã
ctxtime
ããã±ã¼ã¸ã«ã¯ã次ã®ããã«Now
é¢æ°ã ããé
ç½®ãã¾ãã
// ctxtime/ctxtime.go package ctxtime import ( "context" "time" "github.com/newmo-oss/ctxtime/internal" ) func Now(ctx context.Context) time.Time { return internal.Now(ctx) }
大é¨åã®å¦çã¯internal
ããã±ã¼ã¸ã«ç§»åããã¾ãã
ããã¦ãinternal
ããã±ã¼ã¸ã§ã¯æ¬¡ã®ããã«å®£è¨ãã¦ããã¾ãã
// ctxtime/internal/now.go package internal import ( "context" "time" ) var Now = DefaultNow func DefaultNow(_ context.Context) time.Time { return time.Now().In(time.UTC) }
ctxtime.Now
é¢æ°ã®æåãå¤æ´ã§ããããã«ãinternal.Now
ã¯å¤æ°ã¨ãã¦å®£è¨ãã¾ãã
ããã©ã«ãå¤ã¨ãã¦internal.DefaultNow
é¢æ°ãè¨å®ããã¦ãã¾ãã
ããã¦ã次ã®ããã«ctxtimetest
ããã±ã¼ã¸ã®inité¢æ°ã§internal.Now
å¤æ°ã®å¤ãå¤æ´ãã¦ãã¾ãã
// ctxtime/ctxtimetest/ctxtimetest.go package ctxtimetest import ( "context" "sync" "testing" "time" "github.com/newmo-oss/ctxtime/internal" ) func init() { if testing.Testing() { internal.Now = nowForTest } } func nowForTest(ctx context.Context) time.Time { now, ok := nowFromContext(ctx) if ok { return now } return internal.DefaultNow(ctx) } type ctxkey struct{} func WithFixedNow(t *testing.T, ctx context.Context, tm time.Time) context.Context { t.Helper() return context.WithValue(ctx, ctxkey{}, tm) } func nowFromContext(ctx context.Context) (time.Time, bool) { tm, ok := ctx.Value(ctxkey{}).(time.Time) return tm, ok }
ãã®ããã«ãããã¨ã§ctxtime
ããã±ã¼ã¸ã¯ãtesting
ããã±ã¼ã¸ã«ç´æ¥ä¾åãããã¨ãªããã¹ãã®æã ãæå»ãåºå®ã§ããããã«ãªãã¾ãã
ãã¹ããã¨ã®ID
newmoã§ã¯ããã¹ããã¨ã«test id
ã¨å¼ã°ããIDï¼é常ã¯UUIDï¼ãæ¡çªãã¦ãã¾ãã
ãã®ä»ã«ãã¹ããä¸æã«èå¥ããã«ã¯ã*testing.T
åã®Name
ã¡ã½ãããç¨ããæ¹æ³ãããã¾ãã
ãããããã¹ãï¼ãµããã¹ããå«ãï¼ã§IDãè¤æ°ç¨ããããªããã¨ãæ³å®ãã¦ããã¹ãåã¨ã¯å¥ã«IDãä»ä¸ãããã¨ã«ãã¦ãã¾ãã
ã¾ãããã¹ãæã«test idãHTTPã®ããããgRPCã®ã¡ã¿ãã¼ã¿ã«ä»ä¸ãããã¨ã§ãã©ã®ãã¹ããããªã¯ã¨ã¹ããæ¥ããåããããã«ãã¦ãã¾ãã
testid
ããã±ã¼ã¸ã¯ã以ä¸ã®ãªãã¸ããªã§OSSã§å
¬éããã¦ãã¾ãã
newmoã§éçºã»ä½¿ç¨ãã¦ããctxtime
ããã±ã¼ã¸ãåè¿°ããã³ã³ããã¹ãã¨ãã¼ã使ã£ãæ¹æ³ã§ã¯ãªãã次ã®ããã«ãã®test idã使ç¨ããæ¹æ³ã使ç¨ãã¦ãã¾ãã
WithFixedNow
é¢æ°ã®ä»£ããã«SetFixedNow
é¢æ°ãç¨æãããUnsetFixedNow
é¢æ°ã§è¨å®ããæå»ãåé¤ã§ããããã«ãã¦ãã¾ãã
ãªããUnsetFixedNow
é¢æ°ãå¼ã°ãªãã¦ããã¹ãçµäºæã«(*testing.T).Cleanup
é¢æ°ã§èªåã§åé¤ãããããã«ä½ããã¦ãã¾ãã
// ctxtime/ctxtimetest/ctxtimetest.go package ctxtimetest import ( "context" "sync" "testing" "time" "github.com/newmo-oss/ctxtime/internal" "github.com/newmo-oss/testid" ) var fixedNows sync.Map func init() { if testing.Testing() { internal.Now = nowForTest } } // SetFixedNow fixes the return value of ctxtime.Now. // The fixed current time is set each test id which get from [testid.FromContext]. // If any test id cannot obtain from the context, the test will be fail with t.Fatal. // The fixed current time will be remove by t.Cleanup. func SetFixedNow(t testing.TB, ctx context.Context, tm time.Time) { t.Helper() tid, ok := testid.FromContext(ctx) if !ok { t.Fatal("failed to get test ID from the context") } t.Cleanup(func() { fixedNows.Delete(tid) }) fixedNows.Store(tid, tm) } // UnsetFixedNow removes the fixed current time which was set by [SetFixedNow]. // If any test id cannot obtain from the context, the test will be fail with t.Fatal. func UnsetFixedNow(t testing.TB, ctx context.Context) { t.Helper() tid, ok := testid.FromContext(ctx) if !ok { t.Fatal("failed to get test ID from the context") } fixedNows.Delete(tid) } func loadFixedTime(ctx context.Context) (time.Time, bool) { tid, ok := testid.FromContext(ctx) if !ok { return time.Time{}, false } v, ok := fixedNows.Load(tid) if !ok { return time.Time{}, false } tm, ok := v.(time.Time) if !ok { return time.Time{}, false } return tm, true } func nowForTest(ctx context.Context) time.Time { tm, ok := loadFixedTime(ctx) if !ok { return internal.DefaultNow(ctx) } return tm }
Linterã使ã£ãtime.Nowé¢æ°ã®å¼ã³åºãã®æ¤åº
ctxtime.Now
é¢æ°ãå¹æçã«ä½¿ãããã«ã¯ãããã¸ã§ã¯ãå
¨ä½ã§time.Now
é¢æ°ã§ã¯ãªãã¦ãctxtime.Now
é¢æ°ã使ç¨ããã¨ããã«ã¼ã«ãè¨ããå¿
è¦ãããã¾ãã
ã«ã¼ã«ãå®ã£ã¦ãããã¬ãã¥ã¼ã§ãã§ãã¯ããããã«ãã¦ããã¨ãæéãçµã¤ã«ã¤ã形骸åããã¡ã§ãã
ããã§time.Now
ã使ã£ã¦ããç®æãæ¤åºããctxtimechek
ã¨ããLinterãåããã¦ä½æãã¾ããã
ãªãã次ã®ããã«golang.org/x/tools/go/analysis
ããã±ã¼ã¸ï¼ä»¥ä¸ãgo/analysis
ããã±ã¼ã¸ï¼ãç¨ãã¦ä½æãã¦ãã¾ãã
// ctxtime/ctxtimecheck/ctxtimecheck.go package ctxtimecheck import ( "go/types" "github.com/gostaticanalysis/analysisutil" "github.com/gostaticanalysis/ssainspect" "golang.org/x/tools/go/analysis" ) const doc = "ctxtimecheck finds calling time.Now instead of ctxtime.Now" // Analyzer finds calling time.Now instead of ctxtime.Now. var Analyzer = &analysis.Analyzer{ Name: "ctxtimecheck", Doc: doc, Run: run, Requires: []*analysis.Analyzer{ ssainspect.Analyzer, }, } func run(pass *analysis.Pass) (any, error) { in := pass.ResultOf[ssainspect.Analyzer].(*ssainspect.Inspector) timenow, _ := analysisutil.ObjectOf(pass, "time", "Now").(*types.Func) if timenow == nil { // skip return nil, nil } for in.Next() { c := in.Cursor() if analysisutil.Called(c.Instr, nil, timenow) { pass.Reportf(c.Instr.Pos(), "do not use %s, use ctxtime.Now", timenow.FullName()) } } return nil, nil }
Linterãªã©ã®éç解æãã¼ã«ãgo/analysis
ããã±ã¼ã¸ã«ã¤ãã¦ã¯ã300ãã¼ã¸è¶
ãã§ã¡ãã£ã¨ã ãé·ãã§ãã次ã®ã¹ã©ã¤ããåèã«ãªãã¾ãã
time.Now
é¢æ°ã¯ãã¡ãããtime.Now
é¢æ°ããã¼ã«ã«å¤æ°ã«ä»£å
¥ãã¦å¼ã³åºãã¦ããç®æãæ¤åºã§ãã¾ãã
ç¾å¨ã®å®è£
ã§ã¯ããã±ã¼ã¸å¤æ°ã«ä»£å
¥ããå ´åã¯æ¤åºã§ãã¾ããããä»å¾ã®ãã¼ã¸ã§ã³ã¢ããã§å¯¾å¿äºå®ã§ãã
ctxtimechek
ã¯ã次ã®ããã«go install
ãç¨ãã¦ã¤ã³ã¹ãã¼ã«ãã§ãã¾ãã
$ go install github.com/newmo-oss/ctxtime/ctxtimecheck/cmd/ctxtimecheck@latest
ã¤ã³ã¹ãã¼ã«ããå®è¡å¯è½ãã¡ã¤ã«ã¯ãgo vet
ã³ãã³ãã®-vettool
ãã©ã°ã«çµ¶å¯¾ãã¹ãæå®ãããã¨ã§å©ç¨ã§ãã¾ãã
$ go vet -vettool=$(which ctxtimecheck) ./...
ãªããç¾å¨ã®ctxtimecheck
ã¯çè
ãéçºãã¦ããcalled
ã¨ããéç解æãã¼ã«ã§ãåæ§ã®åä½ããã¾ãã
called
ãå°å
¥ãã¦ããæ¹ã¯ã次ã®ããã«æå®ãããã¨ã§åæ§ã®çµæãå¾ãããã§ãããã
$ go vet -vettool=$(which called) -called.funcs="time.Now" ./...
ãããã«
æ¬ç¨¿ã§ã¯ãnewmoã§æ´»ç¨ãã¦ããgo testæã«æå»ãåºå®ããæ¹æ³ã¨OSSåãã¦ããctxtime
ããã±ã¼ã¸ã¨testid
ããã±ã¼ã¸ã®ç´¹ä»ããã¾ããã
time.Now
é¢æ°ã ãã§ã¯ãªããtime.Ticker
åã使ã£ãã³ã¼ããªã©ããã¹ããã«ããã®ã§ãä»å¾ã®ã¢ãããã¼ãã§å¯¾å¿ã§ãããã¨èãã¦ãã¾ãã
newmoã§ã¯ã¹ãã¼ãæãä¿ã¡ãªããæ°ãããããã¯ããéçºãã¤ã¤ãæè¡ã¸ã®ãã£ã¬ã³ã¸ãæãã¾ãæ¥ã ã®éçºãè¡ã£ã¦ãã¾ãã ããããããããã¯ãéçºã®ä¸ã§çã¾ããã©ã¤ãã©ãªãç¥è¦ã¯ãæãã¾ãæè¡ã³ãã¥ããã£ã«ãè¿ãã§ããã°ã¨èãã¦ãã¾ãã
PR: newmoã§ã¯ã¨ã³ã¸ãã¢ãåéãã¦ãã¾ãï¼ èå³ãããæ¹ã¯ã次ã®æ¡ç¨æ å ±ãã覧ãã ããã