diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 0b6f1aea..f473afff 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,6 +32,7 @@ jobs: - "sql" - "trace" - "worker" + - "fxclock" - "fxcore" - "fxconfig" - "fxcron" diff --git a/.github/workflows/fxclock-ci.yml b/.github/workflows/fxclock-ci.yml new file mode 100644 index 00000000..9419805b --- /dev/null +++ b/.github/workflows/fxclock-ci.yml @@ -0,0 +1,32 @@ +name: "fxclock-ci" + +on: + push: + branches: + - "feat**" + - "fix**" + - "hotfix**" + - "chore**" + paths: + - "fxclock/**.go" + - "fxclock/go.mod" + - "fxclock/go.sum" + pull_request: + types: + - opened + - synchronize + - reopened + branches: + - main + paths: + - "fxclock/**.go" + - "fxclock/go.mod" + - "fxclock/go.sum" + +jobs: + ci: + uses: ./.github/workflows/common-ci.yml + secrets: inherit + with: + module: "fxclock" + go_version: "1.21" diff --git a/fxclock/.golangci.yml b/fxclock/.golangci.yml new file mode 100644 index 00000000..60d036ad --- /dev/null +++ b/fxclock/.golangci.yml @@ -0,0 +1,65 @@ +run: + timeout: 5m + concurrency: 8 + +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - cyclop + - decorder + - dogsled + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - exhaustive + - forbidigo + - forcetypeassert + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - gofmt + - goheader + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosimple + - govet + - grouper + - importas + - ineffassign + - interfacebloat + - loggercheck + - maintidx + - makezero + - misspell + - nestif + - nilerr + - nilnil + - nlreturn + - nolintlint + - nosprintfhostport + - prealloc + - predeclared + - promlinter + - reassign + - staticcheck + - tenv + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - whitespace diff --git a/fxclock/README.md b/fxclock/README.md new file mode 100644 index 00000000..093c39c1 --- /dev/null +++ b/fxclock/README.md @@ -0,0 +1,160 @@ +# Fx Clock Module + +[![ci](https://github.com/ankorstore/yokai/actions/workflows/fxclock-ci.yml/badge.svg)](https://github.com/ankorstore/yokai/actions/workflows/fxclock-ci.yml) +[![go report](https://goreportcard.com/badge/github.com/ankorstore/yokai/fxclock)](https://goreportcard.com/report/github.com/ankorstore/yokai/fxclock) +[![codecov](https://codecov.io/gh/ankorstore/yokai/graph/badge.svg?token=ghUBlFsjhR&flag=fxclock)](https://app.codecov.io/gh/ankorstore/yokai/tree/main/fxclock) +[![Deps](https://img.shields.io/badge/osi-deps-blue)](https://deps.dev/go/github.com%2Fankorstore%2Fyokai%2Ffxclock) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/ankorstore/yokai/fxclock)](https://pkg.go.dev/github.com/ankorstore/yokai/fxclock) + +> [Fx](https://uber-go.github.io/fx/) module for [clockwork](https://github.com/jonboulle/clockwork). + + +* [Installation](#installation) +* [Features](#features) +* [Documentation](#documentation) + * [Dependencies](#dependencies) + * [Loading](#loading) + * [Usage](#usage) + * [Testing](#testing) + * [Global time](#global-time) + * [Time control](#time-control) + + +## Installation + +```shell +go get github.com/ankorstore/yokai/fxclock +``` + +## Features + +This module provides a [clockwork.Clock](https://github.com/jonboulle/clockwork) instance for your application, that you +can use to control time. + +## Documentation + +### Dependencies + +This module is intended to be used alongside the [fxconfig](https://github.com/ankorstore/yokai/tree/main/fxconfig) +module. + +### Loading + +To load the module in your Fx application: + +```go +package main + +import ( + "time" + + "github.com/ankorstore/yokai/fxclock" + "github.com/ankorstore/yokai/fxconfig" + "github.com/jonboulle/clockwork" + "go.uber.org/fx" +) + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxclock.FxClockModule, // load the module + fx.Invoke(func(clock clockwork.Clock) { // invoke the clock + clock.Sleep(3 * time.Second) + }), + ).Run() +} +``` + +### Usage + +This module provides a [clockwork.Clock](https://github.com/jonboulle/clockwork) instance, ready to inject in your code. + +This is particularly useful if you need to control time (set time, fast-forward, ...). + +For example: + +```go +package service + +import ( + "time" + + "github.com/jonboulle/clockwork" +) + +type ExampleService struct { + clock clockwork.Clock +} + +func NewExampleService(clock clockwork.Clock) *ExampleService { + return &ExampleService{ + clock: clock, + } +} + +func (s *ExampleService) Now() string { + return s.clock.Now().String() +} +``` + +See the underlying vendor [documentation](https://github.com/jonboulle/clockwork) for more details. + +### Testing + +This module provides a [*clockwork.FakeClock](https://github.com/jonboulle/clockwork) instance, that will be automatically injected as `clockwork.Clock` in your constructors in `test` mode. + +#### Global time + +By default, the fake clock is set to `time.Now()` (your test execution time). + +You can configure the global time in your test in your testing configuration file (for all your tests), in [RFC3339](https://datatracker.ietf.org/doc/html/rfc3339) format: + +```yaml +# ./configs/config_test.yaml +modules: + clock: + test: + time: "2006-01-02T15:04:05Z07:00" # time in RFC3339 format +``` + +You can also [override this value](https://ankorstore.github.io/yokai/modules/fxconfig/#env-var-substitution), per test, by setting the `MODULES_CLOCK_TEST_TIME` env var. + +#### Time control + +You can `populate` the [*clockwork.FakeClock](https://github.com/jonboulle/clockwork) from your test to control time: + +```go +package service_test + +import ( + "testing" + "time" + + "github.com/ankorstore/yokai/fxsql" + "github.com/foo/bar/internal/service" + "github.com/jonboulle/clockwork" + "go.uber.org/fx" +) + +func TestExampleService(t *testing.T) { + testTime := "2025-03-30T12:00:00Z" + expectedTime, _ := time.Parse(time.RFC3339, testTime) + + t.Setenv("MODULES_CLOCK_TEST_TIME", testTime) + + var svc service.ExampleService + var clock *clockwork.FakeClock + + internal.RunTest(t, fx.Populate(&svc, &clock)) + + // current time as configured above + assert.Equal(t, expectedTime, svc.Now()) // 2025-03-30T12:00:00Z + + clock.Advance(5 * time.Hour) + + // current time is now advanced by 5 hours + assert.Equal(t, expectedTime.Add(5*time.Hour), svc.Now()) // 2025-03-30T17:00:00Z +} +``` + +See [tests example](module_test.go) for more details. diff --git a/fxclock/go.mod b/fxclock/go.mod new file mode 100644 index 00000000..e161ddf1 --- /dev/null +++ b/fxclock/go.mod @@ -0,0 +1,37 @@ +module github.com/ankorstore/yokai/fxclock + +go 1.21 + +require ( + github.com/ankorstore/yokai/config v1.5.0 + github.com/ankorstore/yokai/fxconfig v1.3.0 + github.com/jonboulle/clockwork v0.5.0 + github.com/stretchr/testify v1.10.0 + go.uber.org/fx v1.23.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.19.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/dig v1.18.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/fxclock/go.sum b/fxclock/go.sum new file mode 100644 index 00000000..e8eeadb9 --- /dev/null +++ b/fxclock/go.sum @@ -0,0 +1,84 @@ +github.com/ankorstore/yokai/config v1.5.0 h1:vL/l0dcnq34FtxE+Up1NvzgcRB0G/vI4Yo/H5PccfN0= +github.com/ankorstore/yokai/config v1.5.0/go.mod h1:C8ggYvcrG+J0Ra2vTtcDCANa8HMf3FdrC0Ek8o3tTEw= +github.com/ankorstore/yokai/fxconfig v1.3.0 h1:kk+RkpgECjZYciN2E3lnVj1dpewRy54JN7k8zErpX88= +github.com/ankorstore/yokai/fxconfig v1.3.0/go.mod h1:NTF2TbT+xZNEzI/iTCQLtY+oS/AJSDAPAqouPgAYzbE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= +go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= +go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/fxclock/module.go b/fxclock/module.go new file mode 100644 index 00000000..e95a16b2 --- /dev/null +++ b/fxclock/module.go @@ -0,0 +1,51 @@ +package fxclock + +import ( + "time" + + "github.com/ankorstore/yokai/config" + "github.com/jonboulle/clockwork" + "go.uber.org/fx" +) + +// ModuleName is the module name. +const ModuleName = "clock" + +// FxClockModule is the [Fx] clockwork module. +// +// [Fx]: https://github.com/uber-go/fx +var FxClockModule = fx.Module( + ModuleName, + fx.Provide( + NewFxClock, + ), +) + +// FxClockParam allows injection of the required dependencies in [NewFxClock]. +type FxClockParam struct { + fx.In + Config *config.Config +} + +// NewFxClock returns a new [clockwork.Clock] instance. +func NewFxClock(p FxClockParam) (clockwork.Clock, *clockwork.FakeClock, error) { + if p.Config.IsTestEnv() { + testTimeCfg := p.Config.GetString("modules.clock.test.time") + if testTimeCfg == "" { + testClock := clockwork.NewFakeClock() + + return testClock, testClock, nil + } + + testTime, err := time.Parse(time.RFC3339, testTimeCfg) + if err != nil { + return nil, nil, err + } + + testClock := clockwork.NewFakeClockAt(testTime) + + return testClock, testClock, nil + } + + return clockwork.NewRealClock(), nil, nil +} diff --git a/fxclock/module_test.go b/fxclock/module_test.go new file mode 100644 index 00000000..3cb5b11c --- /dev/null +++ b/fxclock/module_test.go @@ -0,0 +1,133 @@ +package fxclock_test + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxclock" + "github.com/ankorstore/yokai/fxclock/testdata/service" + "github.com/ankorstore/yokai/fxconfig" + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/assert" + "go.uber.org/fx" + "go.uber.org/fx/fxtest" +) + +func TestFxClockworkClockModule(t *testing.T) { + t.Setenv("APP_ENV", config.AppEnvDev) + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + runTest := func(tb testing.TB) (clockwork.Clock, *clockwork.FakeClock, *service.TestService) { + tb.Helper() + + var clock clockwork.Clock + var fakeClock *clockwork.FakeClock + var srv *service.TestService + + app := fxtest.New( + tb, + fx.NopLogger, + fxconfig.FxConfigModule, + fxclock.FxClockModule, + fx.Provide(service.NewTestService), + fx.Populate(&clock, &fakeClock, &srv), + ) + + app.RequireStart().RequireStop() + assert.NoError(tb, app.Err()) + + return clock, fakeClock, srv + } + + t.Run("normal mode", func(t *testing.T) { + clock, fakeClock, srv := runTest(t) + + assert.NotNil(t, clock) + assert.Implements(t, (*clockwork.Clock)(nil), clock) + assert.Equal(t, "*clockwork.realClock", fmt.Sprintf("%T", clock)) + + assert.Nil(t, fakeClock) + + assert.NotNil(t, srv) + }) + + t.Run("test mode with default time", func(t *testing.T) { + t.Setenv("APP_ENV", config.AppEnvTest) + + clock, fakeClock, srv := runTest(t) + assert.NotNil(t, clock) + assert.Implements(t, (*clockwork.Clock)(nil), clock) + assert.Equal(t, "*clockwork.FakeClock", fmt.Sprintf("%T", clock)) + + assert.NotNil(t, fakeClock) + assert.Implements(t, (*clockwork.Clock)(nil), fakeClock) + assert.Equal(t, "*clockwork.FakeClock", fmt.Sprintf("%T", fakeClock)) + + assert.NotNil(t, srv) + + startTime := srv.Now() + fakeClock.Advance(10 * time.Minute) + + assert.Equal(t, startTime.Add(10*time.Minute), srv.Now()) + }) + + t.Run("with test clock and fixed time", func(t *testing.T) { + testTime := "2025-03-30T12:00:00Z" + + t.Setenv("APP_ENV", config.AppEnvTest) + t.Setenv("MODULES_CLOCK_TEST_TIME", testTime) + + clock, fakeClock, srv := runTest(t) + assert.NotNil(t, clock) + assert.Implements(t, (*clockwork.Clock)(nil), clock) + assert.Equal(t, "*clockwork.FakeClock", fmt.Sprintf("%T", clock)) + + assert.NotNil(t, fakeClock) + assert.Implements(t, (*clockwork.Clock)(nil), fakeClock) + assert.Equal(t, "*clockwork.FakeClock", fmt.Sprintf("%T", fakeClock)) + + assert.NotNil(t, srv) + + expectedTime, _ := time.Parse(time.RFC3339, testTime) + assert.Equal(t, expectedTime, srv.Now()) + + fakeClock.Advance(5 * time.Hour) + assert.Equal(t, expectedTime.Add(5*time.Hour), srv.Now()) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + srv.Sleep(3 * time.Second) + wg.Done() + }() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + err := fakeClock.BlockUntilContext(ctx, 1) + assert.NoError(t, err) + + fakeClock.Advance(10 * time.Second) + wg.Wait() + }) + + t.Run("test mode with invalid time", func(t *testing.T) { + testTime := "invalid" + t.Setenv("APP_ENV", config.AppEnvTest) + t.Setenv("MODULES_CLOCK_TEST_TIME", testTime) + + app := fx.New( + fx.NopLogger, + fxconfig.FxConfigModule, + fxclock.FxClockModule, + fx.Invoke(func(clock clockwork.Clock) {}), + ) + + err := app.Start(context.Background()) + assert.Error(t, err) + assert.Contains(t, err.Error(), fmt.Sprintf("cannot parse %q", testTime)) + }) +} diff --git a/fxclock/testdata/config/config.dev.yaml b/fxclock/testdata/config/config.dev.yaml new file mode 100644 index 00000000..d158fdb7 --- /dev/null +++ b/fxclock/testdata/config/config.dev.yaml @@ -0,0 +1,6 @@ +app: + env: dev +modules: + log: + level: debug + output: test diff --git a/fxclock/testdata/config/config.test.yaml b/fxclock/testdata/config/config.test.yaml new file mode 100644 index 00000000..11807d58 --- /dev/null +++ b/fxclock/testdata/config/config.test.yaml @@ -0,0 +1,6 @@ +app: + env: test +modules: + log: + level: debug + output: test diff --git a/fxclock/testdata/config/config.yaml b/fxclock/testdata/config/config.yaml new file mode 100644 index 00000000..9e9314ba --- /dev/null +++ b/fxclock/testdata/config/config.yaml @@ -0,0 +1,2 @@ +app: + name: test-app diff --git a/fxclock/testdata/service/service.go b/fxclock/testdata/service/service.go new file mode 100644 index 00000000..5abbd937 --- /dev/null +++ b/fxclock/testdata/service/service.go @@ -0,0 +1,23 @@ +package service + +import ( + "time" + + "github.com/jonboulle/clockwork" +) + +type TestService struct { + clock clockwork.Clock +} + +func NewTestService(clock clockwork.Clock) *TestService { + return &TestService{clock: clock} +} + +func (s *TestService) Now() time.Time { + return s.clock.Now() +} + +func (s *TestService) Sleep(d time.Duration) { + s.clock.Sleep(d) +} diff --git a/release-please-config.json b/release-please-config.json index 1127d273..e98422b0 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -57,6 +57,11 @@ "component": "worker", "tag-separator": "/" }, + "fxclock": { + "release-type": "go", + "component": "fxclock", + "tag-separator": "/" + }, "fxcore": { "release-type": "go", "component": "fxcore",