├── version.txt ├── .github ├── release.yml └── workflows │ ├── tagpr.yml │ └── testandvet.yml ├── _examples └── greeting │ ├── README.md │ ├── go.mod │ ├── greeting.go │ ├── go.sum │ └── greeting_test.go ├── go.mod ├── overlayed.go ├── go.sum ├── cmd └── testtime │ ├── _partials │ └── testtime.go │ ├── testtime.go │ └── overlay.go ├── time_test.go ├── LICENSE ├── time.go ├── .tagpr ├── CHANGELOG.md └── README.md /version.txt: -------------------------------------------------------------------------------- 1 | v0.3.2 2 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - tagpr 5 | -------------------------------------------------------------------------------- /_examples/greeting/README.md: -------------------------------------------------------------------------------- 1 | # greeting 2 | 3 | ```sh 4 | $ go test -overlay=`testtime` 5 | PASS 6 | ok greeting 0.111s 7 | ``` 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tenntenn/testtime 2 | 3 | go 1.23.2 4 | 5 | require golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e 6 | 7 | retract v0.1.0 8 | -------------------------------------------------------------------------------- /_examples/greeting/go.mod: -------------------------------------------------------------------------------- 1 | module greeting 2 | 3 | go 1.23.2 4 | 5 | require github.com/tenntenn/testtime v0.0.0-00010101000000-000000000000 6 | 7 | replace github.com/tenntenn/testtime => ../../ 8 | -------------------------------------------------------------------------------- /_examples/greeting/greeting.go: -------------------------------------------------------------------------------- 1 | package greeting 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | func Do() string { 8 | t := time.Now() 9 | switch h := t.Hour(); { 10 | case h >= 4 && h <= 9: 11 | return "おはよう" 12 | case h >= 10 && h <= 16: 13 | return "こんにちは" 14 | default: 15 | return "こんばんは" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /overlayed.go: -------------------------------------------------------------------------------- 1 | package testtime 2 | 3 | import ( 4 | _ "unsafe" // for go:linkname 5 | ) 6 | 7 | //go:linkname overlayed time.overlayed 8 | var overlayed bool 9 | 10 | // Overlayed returns whether time.go in time package was overlayed by testtime or not. 11 | func Overlayed() bool { 12 | return overlayed 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/tagpr.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/tagpr.yml 2 | name: tagpr 3 | on: 4 | push: 5 | branches: ["main"] 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: Songmu/tagpr@main 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /_examples/greeting/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 2 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 3 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 4 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 5 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 6 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 7 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 2 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 3 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 4 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 5 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 6 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ= 7 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 8 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 9 | -------------------------------------------------------------------------------- /cmd/testtime/_partials/testtime.go: -------------------------------------------------------------------------------- 1 | // It will be added to GOROOT/src/time/time.go. 2 | 3 | //go:linkname timeMap 4 | var timeMap sync.Map 5 | 6 | //go:linkname overlayed 7 | var overlayed = true 8 | 9 | // Now returns a fixed time which is related with the goroutine by SetTime or SetFunc. 10 | // If the current goroutine is not related with any fixed time or function, Now calls time.Now and returns its returned value. 11 | func Now() Time { 12 | v, ok := timeMap.Load(goroutineID()) 13 | if ok { 14 | return v.(func() Time)() 15 | } 16 | return _Now() 17 | } 18 | 19 | func goroutineID() string { 20 | var buf [64]byte 21 | n := runtime.Stack(buf[:], false) 22 | // 10: len("goroutine ") 23 | for i := 10; i < n; i++ { 24 | if buf[i] == ' ' { 25 | return string(buf[10:i]) 26 | } 27 | } 28 | return "" 29 | } 30 | 31 | // End of testtime's code 32 | -------------------------------------------------------------------------------- /time_test.go: -------------------------------------------------------------------------------- 1 | package testtime_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | _ "unsafe" 7 | 8 | "github.com/tenntenn/testtime" 9 | ) 10 | 11 | func Test(t *testing.T) { 12 | 13 | t.Run("SetTime", func(t *testing.T) { 14 | tm := parseTime(t, "2022/11/17 17:21:00") 15 | testtime.SetTime(t, tm) 16 | if !testtime.Now().Equal(tm) { 17 | t.Error("testtime.Now() must be", tm) 18 | } 19 | }) 20 | 21 | t.Run("SetFunc", func(t *testing.T) { 22 | tm := parseTime(t, "2022/11/17 17:23:00") 23 | testtime.SetFunc(t, func() time.Time { return tm }) 24 | 25 | if !testtime.Now().Equal(tm) { 26 | t.Error("testtime.Now() must be", tm) 27 | } 28 | }) 29 | 30 | testtime.SetTime(t, time.Time{}) 31 | if !testtime.Now().IsZero() { 32 | t.Error("testtime.Now() must be zero value") 33 | } 34 | } 35 | 36 | func parseTime(t *testing.T, s string) time.Time { 37 | t.Helper() 38 | tm, err := time.Parse("2006/01/02 15:04:05", s) 39 | if err != nil { 40 | t.Fatal("unexpected error:", err) 41 | } 42 | return tm 43 | } 44 | -------------------------------------------------------------------------------- /_examples/greeting/greeting_test.go: -------------------------------------------------------------------------------- 1 | package greeting_test 2 | 3 | import ( 4 | "greeting" 5 | "testing" 6 | "time" 7 | 8 | "github.com/tenntenn/testtime" 9 | ) 10 | 11 | func TestDo(t *testing.T) { 12 | 13 | if !testtime.Overlayed() { 14 | t.Skip() 15 | } 16 | 17 | t.Parallel() 18 | cases := []struct { 19 | tm string 20 | want string 21 | }{ 22 | {"04:00:00", "おはよう"}, 23 | {"09:00:00", "おはよう"}, 24 | {"10:00:00", "こんにちは"}, 25 | {"16:00:00", "こんにちは"}, 26 | {"17:00:00", "こんばんは"}, 27 | {"03:00:00", "こんばんは"}, 28 | } 29 | 30 | for _, tt := range cases { 31 | tt := tt 32 | t.Run(tt.tm, func(t *testing.T) { 33 | t.Parallel() 34 | testtime.SetTime(t, parseTime(t, tt.tm)) 35 | got := greeting.Do() 36 | if got != tt.want { 37 | t.Errorf("want %s but got %s", tt.want, got) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func parseTime(t *testing.T, v string) time.Time { 44 | t.Helper() 45 | tm, err := time.Parse("2006/01/02 15:04:05", "2006/01/02 "+v) 46 | if err != nil { 47 | t.Fatal("unexpected error:", err) 48 | } 49 | return tm 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/testandvet.yml: -------------------------------------------------------------------------------- 1 | name: Test and Vet 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | release: 11 | types: 12 | - published 13 | - created 14 | - edited 15 | 16 | defaults: 17 | run: 18 | shell: bash 19 | 20 | jobs: 21 | test: 22 | runs-on: ubuntu-20.04 23 | 24 | steps: 25 | - name: Install Go 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: 1.23.x 29 | 30 | - name: Checkout code 31 | uses: actions/checkout@v2 32 | 33 | - name: Cache Go module and build cache 34 | uses: actions/cache@v2 35 | with: 36 | key: go-${{ hashFiles('**/go.sum') }} 37 | path: | 38 | ~/go/pkg/mod 39 | restore-keys: | 40 | go- 41 | 42 | - name: Install tennvet 43 | run: | 44 | GOBIN=$(pwd) go install github.com/tenntenn/tennvet@latest 45 | 46 | - name: Test and vet 47 | run: | 48 | go vet ./... 49 | go vet -vettool=$(pwd)/tennvet ./... 50 | go test -v -race ./... 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Takuya Ueda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/testtime/testtime.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "go/build" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | flagCacheDir string 14 | flagUpdate bool 15 | ) 16 | 17 | func init() { 18 | flag.StringVar(&flagCacheDir, "dir", defaultCacheDir(), "cache directory for testtime") 19 | flag.BoolVar(&flagUpdate, "u", false, "update exsiting files") 20 | } 21 | 22 | func main() { 23 | flag.Parse() 24 | if err := run(); err != nil { 25 | fmt.Fprintln(os.Stderr, "Error:", err) 26 | os.Exit(1) 27 | } 28 | } 29 | 30 | func run() error { 31 | 32 | modroot := flag.Arg(0) 33 | if modroot == "" { 34 | wd, err := os.Getwd() 35 | if err != nil { 36 | return err 37 | } 38 | modroot = wd 39 | } 40 | 41 | overlay, err := createOverlay(flagUpdate, modroot, flagCacheDir) 42 | if err != nil { 43 | return err 44 | } 45 | fmt.Println(overlay) 46 | 47 | return nil 48 | } 49 | 50 | func defaultCacheDir() string { 51 | if envGOPATH := os.Getenv("GOPATH"); envGOPATH != "" { 52 | gopath := strings.Split(envGOPATH, string(os.PathListSeparator)) 53 | return filepath.Join(gopath[0], "pkg", "testtime") 54 | } 55 | return filepath.Join(build.Default.GOPATH, "pkg", "testtime") 56 | } 57 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package testtime 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | "testing" 7 | "time" 8 | _ "unsafe" // for go:linkname 9 | ) 10 | 11 | //go:linkname timeMap time.timeMap 12 | var timeMap sync.Map 13 | 14 | // SetTime stores a fixed time which can be identified by the caller's goroutine. 15 | // The fixed time will be deleted at the end of test. 16 | func SetTime(t *testing.T, tm time.Time) { 17 | t.Helper() 18 | 19 | key := goroutineID() 20 | timeMap.Store(key, func() time.Time { 21 | return tm 22 | }) 23 | 24 | t.Cleanup(func() { 25 | timeMap.Delete(key) 26 | }) 27 | } 28 | 29 | // SetFunc stores a function which returns time.Time which can be identified by the caller's goroutine. 30 | // The function will be deleted at the end of test. 31 | func SetFunc(t *testing.T, f func() time.Time) { 32 | t.Helper() 33 | 34 | key := goroutineID() 35 | timeMap.Store(key, f) 36 | 37 | t.Cleanup(func() { 38 | timeMap.Delete(key) 39 | }) 40 | } 41 | 42 | // Now returns a fixed time which is related with the goroutine by SetTime or SetFunc. 43 | // If the current goroutine is not related with any fixed time or function, Now calls time.Now and returns its returned value. 44 | func Now() time.Time { 45 | v, ok := timeMap.Load(goroutineID()) 46 | if ok { 47 | return v.(func() time.Time)() 48 | } 49 | return time.Now() 50 | } 51 | 52 | func goroutineID() string { 53 | var buf [64]byte 54 | n := runtime.Stack(buf[:], false) 55 | // 10: len("goroutine ") 56 | for i := 10; i < n; i++ { 57 | if buf[i] == ' ' { 58 | return string(buf[10:i]) 59 | } 60 | } 61 | return "" 62 | } 63 | -------------------------------------------------------------------------------- /.tagpr: -------------------------------------------------------------------------------- 1 | # config file for the tagpr in git config format 2 | # The tagpr generates the initial configuration, which you can rewrite to suit your environment. 3 | # CONFIGURATIONS: 4 | # tagpr.releaseBranch 5 | # Generally, it is "main." It is the branch for releases. The pcpr tracks this branch, 6 | # creates or updates a pull request as a release candidate, or tags when they are merged. 7 | # 8 | # tagpr.versionFile 9 | # Versioning file containing the semantic version needed to be updated at release. 10 | # It will be synchronized with the "git tag". 11 | # Often this is a meta-information file such as gemspec, setup.cfg, package.json, etc. 12 | # Sometimes the source code file, such as version.go or Bar.pm, is used. 13 | # If you do not want to use versioning files but only git tags, specify the "-" string here. 14 | # You can specify multiple version files by comma separated strings. 15 | # 16 | # tagpr.vPrefix 17 | # Flag whether or not v-prefix is added to semver when git tagging. (e.g. v1.2.3 if true) 18 | # This is only a tagging convention, not how it is described in the version file. 19 | # 20 | # tagpr.changelog (Optional) 21 | # Flag whether or not changelog is added or changed during the release. 22 | # 23 | # tagpr.command (Optional) 24 | # Command to change files just before release. 25 | # 26 | # tagpr.tmplate (Optional) 27 | # Pull request template in go template format 28 | # 29 | # tagpr.release (Optional) 30 | # GitHub Release creation behavior after tagging [true, draft, false] 31 | # If this value is not set, the release is to be created. 32 | [tagpr] 33 | vPrefix = true 34 | releaseBranch = main 35 | versionFile = version.txt 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.3.2](https://github.com/tenntenn/testtime/compare/v0.3.1...v0.3.2) - 2024-10-23 4 | - Fix GOROOT of build.Context by @tenntenn in https://github.com/tenntenn/testtime/pull/20 5 | 6 | ## [v0.3.1](https://github.com/tenntenn/testtime/compare/v0.3.0...v0.3.1) - 2024-10-23 7 | - Fix goroot by @tenntenn in https://github.com/tenntenn/testtime/pull/18 8 | - Update to v0.3.1 by @tenntenn in https://github.com/tenntenn/testtime/pull/17 9 | 10 | ## [v0.3.0](https://github.com/tenntenn/testtime/compare/v0.2.2...v0.3.0) - 2024-09-30 11 | - Add tagpr workflow by @tenntenn in https://github.com/tenntenn/testtime/pull/9 12 | - Remo function name from map keys by @tenntenn in https://github.com/tenntenn/testtime/pull/8 13 | - fix go:linkname issue by @shogo82148 in https://github.com/tenntenn/testtime/pull/15 14 | - Fix overlay go version by @tenntenn in https://github.com/tenntenn/testtime/pull/16 15 | - Add Overlayed() by @tenntenn in https://github.com/tenntenn/testtime/pull/11 16 | 17 | ## [v0.2.2](https://github.com/tenntenn/testtime/compare/v0.2.1...v0.2.2) - 2021-07-05 18 | - Add goroutine id to key by @tenntenn in https://github.com/tenntenn/testtime/pull/5 19 | - Fix README by @tenntenn in https://github.com/tenntenn/testtime/pull/6 20 | - Add example by @tenntenn in https://github.com/tenntenn/testtime/pull/7 21 | 22 | ## [v0.2.1](https://github.com/tenntenn/testtime/compare/v0.2.0...v0.2.1) - 2021-07-05 23 | 24 | ## [v0.2.0](https://github.com/tenntenn/testtime/compare/v0.1.0...v0.2.0) - 2021-07-05 25 | - Rename Se to SetTime and Add SetFunc by @tenntenn in https://github.com/tenntenn/testtime/pull/4 26 | 27 | ## [v0.1.0](https://github.com/tenntenn/testtime/commits/v0.1.0) - 2021-07-05 28 | - Create testandvet.yml by @tenntenn in https://github.com/tenntenn/testtime/pull/1 29 | - Add gotesttime by @tenntenn in https://github.com/tenntenn/testtime/pull/2 30 | - Fix command by @tenntenn in https://github.com/tenntenn/testtime/pull/3 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # testtime 2 | 3 | [![pkg.go.dev][gopkg-badge]][gopkg] 4 | 5 | ## How to use 6 | 7 | `testtime` package provides `testtime.Now()` and `testtime.SetTime()`. 8 | `testtime.SetTime()` stores a fixed time to a map with goroutine ID of `testtime.SetTime()` as a key. 9 | When goroutine ID of `testtime.Now()` is related to a fixed time by `testtime.SetTime()`, it returns the fixed time otherwise it returns current time which is returned by `time.Now()`. 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | "time" 17 | "testing" 18 | 19 | "github.com/tenntenn/testtime" 20 | ) 21 | 22 | func Test(t *testing.T) { 23 | 24 | t.Run("A", func(t *testing.T) { 25 | // set zero value 26 | testtime.SetTime(t, time.Time{}) 27 | // true 28 | if time.Now().IsZero { 29 | t.Error("error") 30 | } 31 | }) 32 | 33 | t.Run("B", func(t *testing.T) { 34 | // set func which return zero value 35 | f := func() time.Time { 36 | return time.Time{} 37 | } 38 | testtime.SetFunc(t, f) 39 | // true 40 | if time.Now().IsZero { 41 | t.Error("error") 42 | } 43 | }) 44 | 45 | // false 46 | if !time.Now().IsZero { 47 | t.Error("error") 48 | } 49 | } 50 | ``` 51 | 52 | The `testtime` command replace `time.Now` to `testtime.Now`. 53 | It prints a file path of overlay JSON which can be given to `-overlay` flag of `go test` command. 54 | 55 | ```sh 56 | $ go test -overlay=$(go run github.com/tenntenn/testtime/cmd/testtime@latest) 57 | PASS 58 | ok main 0.156s 59 | ``` 60 | 61 | You can install `testtime` with the `go install` command. 62 | And don't forget to add the installed directory to the PATH environment variable. 63 | 64 | ```sh 65 | $ go install github.com/tenntenn/testtime/cmd/testtime@latest 66 | ``` 67 | 68 | The `testtime` command creates an overlay JSON file and `time.go` which is replaced `time.Now` in `$GOPATH/pkg/testtime` directory. The `testtime` command does not update these files without `-u` flag. 69 | 70 | ```sh 71 | $ cat `testtime` | jq 72 | { 73 | "Replace": { 74 | "/usr/local/go/src/time/time.go": "/Users/tenntenn/go/pkg/testtime/time_go1.23.1.go" 75 | } 76 | } 77 | 78 | $ diff /usr/local/go/src/time/time.go /Users/tenntenn/go/pkg/testtime/time_go1.23.1.go 79 | 94a95,96 80 | > "runtime" 81 | > "sync" 82 | 1159c1161 83 | < func Now() Time { 84 | --- 85 | > func _Now() Time { 86 | 1695a1698,1726 87 | > 88 | > // It will be added to GOROOT/src/time/time.go. 89 | > 90 | > //go:linkname timeMap 91 | > var timeMap sync.Map 92 | > 93 | > // Now returns a fixed time which is related with the goroutine by SetTime or SetFunc. 94 | > // If the current goroutine is not related with any fixed time or function, Now calls time.Now and returns its returned value. 95 | > func Now() Time { 96 | > v, ok := timeMap.Load(goroutineID()) 97 | > if ok { 98 | > return v.(func() Time)() 99 | > } 100 | > return _Now() 101 | > } 102 | > 103 | > func goroutineID() string { 104 | > var buf [64]byte 105 | > n := runtime.Stack(buf[:], false) 106 | > // 10: len("goroutine ") 107 | > for i := 10; i < n; i++ { 108 | > if buf[i] == ' ' { 109 | > return string(buf[10:i]) 110 | > } 111 | > } 112 | > return "" 113 | > } 114 | > 115 | > // End of testtime's code 116 | ``` 117 | 118 | ## Examples 119 | 120 | See [_examples](./_examples) directory. 121 | 122 | 123 | [gopkg]: https://pkg.go.dev/github.com/tenntenn/testtime 124 | [gopkg-badge]: https://pkg.go.dev/badge/github.com/tenntenn/testtime?status.svg 125 | -------------------------------------------------------------------------------- /cmd/testtime/overlay.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "go/ast" 10 | "go/build" 11 | "go/format" 12 | "go/parser" 13 | "go/token" 14 | "io" 15 | "os" 16 | "os/exec" 17 | "path/filepath" 18 | "strings" 19 | 20 | "golang.org/x/tools/go/ast/astutil" 21 | ) 22 | 23 | //go:embed _partials/testtime.go 24 | var testtime string 25 | 26 | func goVersion(modroot string) (string, error) { 27 | var stdout bytes.Buffer 28 | cmd := exec.Command("go", "env", "GOVERSION") 29 | cmd.Dir = modroot 30 | cmd.Stdout = &stdout 31 | 32 | if err := cmd.Run(); err != nil { 33 | return "", err 34 | } 35 | 36 | return strings.TrimSpace(stdout.String()), nil 37 | } 38 | 39 | func goRoot(modroot string) (string, error) { 40 | var stdout bytes.Buffer 41 | cmd := exec.Command("go", "env", "GOROOT") 42 | cmd.Dir = modroot 43 | cmd.Stdout = &stdout 44 | 45 | if err := cmd.Run(); err != nil { 46 | return "", err 47 | } 48 | 49 | return strings.TrimSpace(stdout.String()), nil 50 | } 51 | 52 | func createOverlay(update bool, modroot, output string) (string, error) { 53 | 54 | ver, err := goVersion(modroot) 55 | if err != nil { 56 | return "", err 57 | } 58 | 59 | overlay := filepath.Join(output, fmt.Sprintf("overlay_%s.json", ver)) 60 | _, err = os.Stat(overlay) 61 | switch { 62 | case err == nil: 63 | if !update { 64 | return overlay, nil 65 | } 66 | case !errors.Is(err, os.ErrNotExist): 67 | return "", err 68 | } 69 | 70 | if err := os.MkdirAll(output, 0o700); err != nil { 71 | return "", err 72 | } 73 | 74 | goroot, err := goRoot(modroot) 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | var buf bytes.Buffer 80 | old, err := replaceTimeNow(&buf, goroot) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | fmt.Fprint(&buf, testtime) 86 | 87 | src, err := format.Source(buf.Bytes()) 88 | if err != nil { 89 | return "", err 90 | } 91 | 92 | new := filepath.Join(output, fmt.Sprintf("time_%s.go", ver)) 93 | if err := os.WriteFile(new, src, 0o600); err != nil { 94 | return "", err 95 | } 96 | 97 | v := struct { 98 | Replace map[string]string 99 | }{map[string]string{old: new}} 100 | jsonBytes, err := json.Marshal(v) 101 | if err != nil { 102 | return "", err 103 | } 104 | if err := os.WriteFile(overlay, jsonBytes, 0o600); err != nil { 105 | return "", err 106 | } 107 | 108 | return overlay, nil 109 | } 110 | 111 | func replaceTimeNow(w io.Writer, goroot string) (string, error) { 112 | srcDir := filepath.Join(goroot, "src") 113 | ctx := build.Default 114 | ctx.GOROOT = goroot 115 | pkg, err := ctx.Import("time", srcDir, 0) 116 | if err != nil { 117 | return "", err 118 | } 119 | 120 | fset := token.NewFileSet() 121 | pkgs, err := parser.ParseDir(fset, pkg.Dir, nil, parser.ParseComments) 122 | if err != nil { 123 | return "", err 124 | } 125 | 126 | if pkgs["time"] == nil { 127 | return "", errors.New("cannot find time package") 128 | } 129 | 130 | var ( 131 | path string 132 | syntax *ast.File 133 | ) 134 | LOOP: 135 | for name, file := range pkgs["time"].Files { 136 | for _, decl := range file.Decls { 137 | decl, _ := decl.(*ast.FuncDecl) 138 | if decl == nil { 139 | continue 140 | } 141 | 142 | if decl.Name.Name == "Now" { 143 | decl.Name.Name = "_Now" 144 | path = name 145 | syntax = file 146 | break LOOP 147 | } 148 | } 149 | } 150 | 151 | if path == "" || syntax == nil { 152 | return "", errors.New("cannot find time.Now") 153 | } 154 | 155 | astutil.AddImport(fset, syntax, "sync") 156 | astutil.AddImport(fset, syntax, "runtime") 157 | 158 | if err := format.Node(w, fset, syntax); err != nil { 159 | return "", err 160 | } 161 | 162 | return path, nil 163 | } 164 | --------------------------------------------------------------------------------