diff --git a/executors_test.go b/executors_test.go index 05ac4ab2..c3340b59 100644 --- a/executors_test.go +++ b/executors_test.go @@ -1989,28 +1989,3 @@ func Test_Delete(t *testing.T) { r.Equal(count, ctx) }) } - -func Test_Create_Timestamps_With_NowFunc(t *testing.T) { - if PDB == nil { - t.Skip("skipping integration tests") - } - transaction(func(tx *Connection) { - r := require.New(t) - - originalNowFunc := nowFunc - // ensure the original function is restored - defer func() { - nowFunc = originalNowFunc - }() - - fakeNow, _ := time.Parse(time.RFC3339, "2019-07-14T00:00:00Z") - SetNowFunc(func() time.Time { return fakeNow }) - - friend := Friend{FirstName: "Yester", LastName: "Day"} - err := tx.Create(&friend) - r.NoError(err) - - r.Equal(fakeNow, friend.CreatedAt) - r.Equal(fakeNow, friend.UpdatedAt) - }) -} diff --git a/model.go b/model.go index 4a100491..2c2fb2b4 100644 --- a/model.go +++ b/model.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "reflect" + "regexp" + "runtime" "strings" "time" @@ -15,8 +17,36 @@ import ( var nowFunc = time.Now +func ensuredCalledInsideInit() { + // If the call stack is deeper than 1024... we got bigger problems. + callers := [1024]uintptr{} + callersCount := runtime.Callers(0, callers[:]) + frames := runtime.CallersFrames(callers[:callersCount]) + + // The `init()` function gets compiled to `path/to/pkg.init.0` where + // the final number varies. + re := regexp.MustCompile(`\.init\.\d+$`) + + for { + frame, ok := frames.Next() + if !ok { + break + } + if re.Match([]byte(frame.Func.Name())) { + return + } + } + + panic("caller init() not found in call stack, reached end of call stack") +} + // SetNowFunc allows an override of time.Now for customizing CreatedAt/UpdatedAt func SetNowFunc(f func() time.Time) { + // From the Go spec: + // > the invocation of init functions—happens in a single goroutine, sequentially, one package at a time + // Since this function mutates a global variable, it should only be called inside `init()`. + ensuredCalledInsideInit() + nowFunc = f }