diff --git a/clock/Clock.go b/clock/Clock.go index 353f01c..41d57f0 100644 --- a/clock/Clock.go +++ b/clock/Clock.go @@ -1,284 +1,48 @@ package clock import ( - "runtime" - "sync" "time" "go.llib.dev/testcase/clock/internal" ) -// Now returns the current time. -// Time returned by Now is affected by time travelling. +// Now returns the current local time. +// +// During testing, Time returned by Now is affected by time travelling. func Now() time.Time { - return internal.TimeNow().Local() + return internal.NowFunc() } -// TimeNow is an alias for clock.Now -func TimeNow() time.Time { return Now() } - +// Sleep pauses the current goroutine for at least the duration d. +// A negative or zero duration causes Sleep to return immediately. +// +// During testing, it will react to time travelling events func Sleep(d time.Duration) { - <-After(d) + internal.SleepFunc(d) } -func After(d time.Duration) <-chan struct{} { - startedAt := internal.TimeNow() - ch := make(chan struct{}) - if d == 0 { - close(ch) - return ch - } - go func() { - timeTravel := make(chan internal.TimeTravelEvent) - defer internal.Notify(timeTravel)() - defer close(ch) - var handleTimeTravel func(tt internal.TimeTravelEvent) bool - handleTimeTravel = func(tt internal.TimeTravelEvent) bool { - deadline := startedAt.Add(d) - if tt.When.After(deadline) || tt.When.Equal(deadline) { - return true - } - if tt.Deep && tt.Freeze { - // wait for next time travel, since during deep freeze, the flow of time is frozen - return handleTimeTravel(<-timeTravel) - } - return false - } - if tt, ok := internal.Check(); ok && tt.Deep && tt.Freeze { - if handleTimeTravel(tt) { - return - } - } - var onWait = func() (_restart bool) { - c, td := after(internal.RemainingDuration(startedAt, d)) - defer td() - select { - case tt := <-timeTravel: - return !handleTimeTravel(tt) - case <-c: - return false - } - } - for onWait() { - } - }() - return ch +// After waits for the duration to elapse and then sends the current time on the returned channel. +// The underlying Timer is not recovered by the garbage collector +// +// During testing, After will react to time travelling. +func After(d time.Duration) <-chan time.Time { + return internal.After(d) } +// NewTicker returns a new Ticker containing a channel that will send +// the current time on the channel after each tick. The period of the +// ticks is specified by the duration argument. The ticker will adjust +// the time interval or drop ticks to make up for slow receivers. +// The duration d must be greater than zero; if not, NewTicker will +// panic. Stop the ticker to release associated resources. +// +// During testing, Ticker will react to time travelling. func NewTicker(d time.Duration) *Ticker { - ticker := &Ticker{d: d} - ticker.init() - return ticker -} - -type Ticker struct { - C chan time.Time - - d time.Duration - - onInit sync.Once - lock sync.RWMutex // is lock really needed if only the background goroutine reads the values from it? - done chan struct{} - pulse chan struct{} - ticker *time.Ticker - lastTickedAt time.Time -} - -func (t *Ticker) init() { - t.onInit.Do(func() { - t.C = make(chan time.Time) - t.done = make(chan struct{}) - t.pulse = make(chan struct{}) - t.ticker = time.NewTicker(t.getScaledDuration()) - t.updateLastTickedAt() - go func() { - timeTravel := make(chan internal.TimeTravelEvent) - defer internal.Notify(timeTravel)() - - if tt, ok := internal.Check(); ok { // trigger initial time travel awareness - if !t.handleTimeTravel(timeTravel, tt) { - return - } - } - - for { - if !t.ticking(timeTravel, t.ticker.C, tickingOption{}) { - break - } - } - }() - }) -} - -type tickingOption struct { - // OnEvent will be executed when an event is received during waiting for ticking - OnEvent func() -} - -func (h tickingOption) onEvent() { - if h.OnEvent == nil { - return - } - h.OnEvent() -} - -func (t *Ticker) ticking(timeTravel <-chan internal.TimeTravelEvent, tick <-chan time.Time, o tickingOption) bool { - select { - case <-t.done: - o.onEvent() - return false - - case tt := <-timeTravel: // on time travel, we reset the ticker according to the new time - o.onEvent() - return t.handleTimeTravel(timeTravel, tt) - - case <-tick: // on time.Ticker tick, we also tick - o.onEvent() - select { - case tt := <-timeTravel: - return t.handleTimeTravel(timeTravel, tt) - case t.C <- t.updateLastTickedAt(): - } - return true - - } -} - -func (t *Ticker) handleTimeTravel(timeTravel <-chan internal.TimeTravelEvent, tt internal.TimeTravelEvent) bool { - var ( - opt = tickingOption{} - prev = tt.Prev - when = tt.When - ) - if lastTickedAt := t.getLastTickedAt(); lastTickedAt.Before(prev) { - prev = lastTickedAt - } - if fn := t.fastForwardTicksTo(prev, when); fn != nil { - opt.OnEvent = fn - } - if tt.Deep && tt.Freeze { - return t.ticking(timeTravel, nil, opt) // wait for unfreeze - } - defer t.resetTicker() - c, td := after(internal.RemainingDuration(t.getLastTickedAt(), t.getRealDuration())) - defer td() - return t.ticking(timeTravel, c, opt) // wait the remaining time from the current tick -} - -func (t *Ticker) fastForwardTicksTo(from, till time.Time) func() { - var travelledDuration = till.Sub(from) - - if travelledDuration <= 0 { - return nil - } - var ( - doneBeforeNextEvent = make(chan struct{}) - fastforwardWG = &sync.WaitGroup{} - timeBetweenTicks = t.getRealDuration() - missingTicks = int(travelledDuration / timeBetweenTicks) - ) - var OnBeforeEvent = func() { - close(doneBeforeNextEvent) - fastforwardWG.Wait() - } - - // fast forward last ticked at position to the time after the ticks - t.updateLastTickedAtTo(from.Add(timeBetweenTicks * time.Duration(missingTicks))) - - fastforwardWG.Add(1) - go func(tickedAt time.Time) { - defer fastforwardWG.Done() - - fastForward: - for i := 0; i < missingTicks; i++ { - tickedAt = tickedAt.Add(timeBetweenTicks) // move to the next tick time - select { - case <-doneBeforeNextEvent: - break fastForward - case t.C <- tickedAt: // tick! - continue fastForward - } - } - }(from) - runtime.Gosched() - - return OnBeforeEvent -} - -// Stop turns off a ticker. After Stop, no more ticks will be sent. -// Stop does not close the channel, to prevent a concurrent goroutine -// reading from the channel from seeing an erroneous "tick". -func (t *Ticker) Stop() { - t.lock.Lock() - defer t.lock.Unlock() - t.init() - close(t.done) - t.ticker.Stop() - t.onInit = sync.Once{} -} - -func (t *Ticker) Reset(d time.Duration) { - t.init() - t.setDuration(d) - t.resetTicker() -} - -func (t *Ticker) resetTicker() { - d := t.getScaledDuration() - if d == 0 { // zero is not an acceptable tick time - d = time.Nanosecond - } - t.ticker.Reset(d) -} - -// getScaledDuration returns the time duration that is altered by time -func (t *Ticker) getScaledDuration() time.Duration { - return internal.ScaledDuration(t.getRealDuration()) + return internal.NewTickerFunc(d) } -func (t *Ticker) getRealDuration() time.Duration { - t.lock.RLock() - defer t.lock.RUnlock() - return t.d -} - -func (t *Ticker) setDuration(d time.Duration) { - t.lock.Lock() - defer t.lock.Unlock() - t.d = d -} - -func (t *Ticker) getLastTickedAt() time.Time { - t.lock.RLock() - defer t.lock.RUnlock() - return t.lastTickedAt -} - -func (t *Ticker) updateLastTickedAt() time.Time { - return t.updateLastTickedAtTo(Now()) -} - -func (t *Ticker) updateLastTickedAtTo(at time.Time) time.Time { - t.lock.RLock() - defer t.lock.RUnlock() - t.lastTickedAt = at - return t.lastTickedAt -} - -func after(d time.Duration) (<-chan time.Time, func()) { - if d <= 0 { - var ch = make(chan time.Time) - close(ch) - return ch, func() {} - } - timer := time.NewTimer(d) - return timer.C, func() { - if !timer.Stop() { - select { - case <-timer.C: // drain channel to unlock the resource - default: - } - } - } -} +// Ticker acts as a proxy between the caller and the ticker implementation. +// During testing, it will be a clock-based ticker that can time travel, +// and outside of testing, it will use the time.Ticker. +type Ticker = internal.TickerProxy diff --git a/clock/Clock_test.go b/clock/Clock_test.go index c99ba21..2a1e4a9 100644 --- a/clock/Clock_test.go +++ b/clock/Clock_test.go @@ -398,7 +398,7 @@ func TestNewTicker(t *testing.T) { }) s.Test("freezing will not affect the frequency of the ticks only the returned time, as ticks often used for background scheduling", func(t *testcase.T) { - timecop.Travel(t, time.Duration(0), timecop.Freeze) + timecop.Travel(t, time.Now(), timecop.Freeze) duration.Set(t, time.Second/10) var ticks int64 @@ -418,9 +418,15 @@ func TestNewTicker(t *testing.T) { const additionalTicks = 10000 timecop.Travel(t, duration.Get(t)*additionalTicks) - runtime.Gosched() - assert.Eventually(t, 2*duration.Get(t), func(t assert.It) { + // this test is very histerical, + // and refuses to have the other goroutine get proper scheduling, + // so here we are, scheduling it ourselves + // and have a very long deadline for assert.Eventually. + assert.Eventually(t, 1000*duration.Get(t), func(t assert.It) { + runtime.Gosched() + time.Sleep(time.Nanosecond) + currentTicks := atomic.LoadInt64(&ticks) expMinTicks := int64(additionalTicks * failureRateMultiplier) t.Log("additional ticks:", additionalTicks) diff --git a/clock/README.md b/clock/README.md index 462c892..d66f9bd 100644 --- a/clock/README.md +++ b/clock/README.md @@ -4,40 +4,52 @@ - [Clock and Timecop](#clock-and-timecop) - [INSTALL](#install) - [FEATURES](#features) + - [Freezing time](#freezing-time) - [USAGE](#usage) - [timecop.Travel + timecop.Freeze](#timecoptravel--timecopfreeze) - [timecop.SetSpeed](#timecopsetspeed) - [Design](#design) - [References](#references) - [FAQ](#faq) + - [What is the performance impact on my production code if I use clock?](#what-is-the-performance-impact-on-my-production-code-if-i-use-clock) - [Why not pass a function argument or time value directly to a function/method?](#why-not-pass-a-function-argument-or-time-value-directly-to-a-functionmethod) - [Will this replace dependency injection for time-related configurations?](#will-this-replace-dependency-injection-for-time-related-configurations) - [Why not just use a global variable with `time.Now`?](#why-not-just-use-a-global-variable-with-timenow) + - [How does the clock package help with testing time-dependent goroutine synchronization?](#how-does-the-clock-package-help-with-testing-time-dependent-goroutine-synchronization) # Clock and Timecop -Package providing "time travel" and "time scaling" capabilities, -making it simple to test time-dependent code. - - +The `clock` package provides advanced "time travel" and "time scaling" features for easy testing of time-dependent code. +As a drop-in replacement for the standard `time` package, it simplifies simulating and manipulating time in your applications, ensuring smooth integration and improved functionality for your time-based tests. ## INSTALL ```sh -go get -u go.llib.dev/testcase +go get go.llib.dev/testcase/clock ``` ## FEATURES +- Drop in replacement for standard `time` package - Freeze time to a specific point. - Travel back to a specific time, but allow time to continue moving forward. - Scale time by a given scaling factor will cause the time to move at an accelerated pace. - No dependencies other than the stdlib -- Nested calls to timecop.Travel is supported +- Continous and nested calls with timecop.Travel are supported - Works with any regular Go projects +## Freezing time + +Using the `timecop` package, you can freeze time in `clock` at different levels during a time travelling. + +The first level, `timecop.Freeze`, stops the timeline from moving forward but doesn't halt tickers or functions that depend on time. This is useful for testing components that depends on background goroutines that has time-based scheduling, allowing events to fire while ensuring you can assert time values in entity fields. +Think of it like freezing a river — the surface is still, but the flow underneath continues. + +The second level, `timecop.DeepFreeze`, completely freezes all time-related values and prevents functions like `clock.Ticker`, `clock.After`, and `clock.Sleep` from firing until time is moved forward. This is useful for testing components with time-sensitive behaviour. +With the river example, deep freeze would mean turning a river into something like a glacier. + ## USAGE ```go @@ -106,6 +118,12 @@ The package was inspired by [travisjeffery' timecop project](https://github.com/ ## FAQ +### What is the performance impact on my production code if I use clock? + +There is no performance impact on your production code. +The time-travel feature is only enabled during testing. +In a production environment, the clock's functions act as aliases for the standard `time` functions. + ### Why not pass a function argument or time value directly to a function/method? While injecting time as an argument or dependency is a valid approach, the aim with `clock` was to keep the usage feeling idiomatic and close to the standard `time` package, while also making testing convenient and easy. @@ -123,4 +141,29 @@ That approach can work well for testing. If you consistently use that global var If you decide to use a global variable, I highly recommend creating a Stub function for it. This function should reset the value to `time.Now` after the test is done, ensuring clean tests cleanups. Also, be mindful of parallel testing and potential edge cases with it. -If you're looking to create a reusable component in the form of a shared package that supports time manipulation in tests, make sure the common package that has the time stub functionality is easily accessible to those using your package. \ No newline at end of file +If you're looking to create a reusable component in the form of a shared package that supports time manipulation in tests, make sure the common package that has the time stub functionality is easily accessible to those using your package. + +### How does the clock package help with testing time-dependent goroutine synchronization? + +The `clock` package focuses on time manipulation rather than testing the implementation details of components using goroutines. +It isn't designed to test time-based synchronization between goroutines, +but rather to enable testing that focuse on the system behaviour. + +For this purpose, you can use the `assert.Eventually` helper from `testcase/assert` to test systems with eventual consistency. +This helper improves the likelihood that your goroutines will be scheduled more eagerly, +increasing the chances that your test assertions will be met in the ideal testing time. + +```go +func TestXXX(t *testing.T) { + subject := ... + + timecop.Travel(t, time.Hour) // trigger time related behavioural change + + assert.Eventually(t, time.Second, func(t assert.It) { // assert that eventually within a second, we expect an outcome + got := subject.LookupSomething(...) + assert.Equal(t, got, "expected") + }) + +} +``` + diff --git a/clock/internal/glob.go b/clock/internal/glob.go new file mode 100644 index 0000000..9b178bd --- /dev/null +++ b/clock/internal/glob.go @@ -0,0 +1,47 @@ +package internal + +import ( + "testing" + "time" +) + +var ( + NowFunc func() time.Time + SleepFunc func(d time.Duration) + AfterFunc func(d time.Duration) <-chan time.Time + NewTickerFunc func(d time.Duration) *TickerProxy +) + +var _ = useTimeFunctions() + +func init() { + if testing.Testing() { // enable time travelling during testing + useClockFunctions() + } +} + +func useTimeFunctions() struct{} { + NowFunc = time.Now + SleepFunc = time.Sleep + AfterFunc = time.After + NewTickerFunc = timeNewTicker + return struct{}{} +} + +func useClockFunctions() { + NowFunc = func() time.Time { + return TimeNow().Local() + } + SleepFunc = func(d time.Duration) { + <-After(d) + } + AfterFunc = After + NewTickerFunc = func(d time.Duration) *TickerProxy { + ticker := NewTicker(d) + return &TickerProxy{ + C: ticker.C, + onStop: ticker.Stop, + onReset: ticker.Reset, + } + } +} diff --git a/clock/internal/impl.go b/clock/internal/impl.go new file mode 100644 index 0000000..93f88d6 --- /dev/null +++ b/clock/internal/impl.go @@ -0,0 +1,290 @@ +package internal + +import ( + "runtime" + "sync" + "time" +) + +func timeNewTicker(d time.Duration) *TickerProxy { + ticker := time.NewTicker(d) + return &TickerProxy{ + C: ticker.C, + onStop: ticker.Stop, + onReset: ticker.Reset, + } +} + +func After(d time.Duration) <-chan time.Time { + startedAt := TimeNow() + ch := make(chan time.Time) + if d == 0 { + go func() { ch <- startedAt }() + return ch + } + go func() { + timeTravel := make(chan TimeTravelEvent) + defer Notify(timeTravel)() + defer close(ch) + var handleTimeTravel func(tt TimeTravelEvent) bool + handleTimeTravel = func(tt TimeTravelEvent) bool { + deadline := startedAt.Add(d) + if tt.When.After(deadline) || tt.When.Equal(deadline) { + return true + } + if tt.Deep && tt.Freeze { + // wait for next time travel, since during deep freeze, the flow of time is frozen + return handleTimeTravel(<-timeTravel) + } + return false + } + if tt, ok := Check(); ok && tt.Deep && tt.Freeze { + if handleTimeTravel(tt) { + return + } + } + var onWait = func() (_restart bool) { + c, td := timeAfterWithCleanup(RemainingDuration(startedAt, d)) + defer td() + select { + case tt := <-timeTravel: + return !handleTimeTravel(tt) + case <-c: + return false + } + } + for onWait() { + } + ch <- TimeNow() + }() + return ch +} + +func timeAfterWithCleanup(d time.Duration) (<-chan time.Time, func()) { + if d <= 0 { + var ch = make(chan time.Time) + close(ch) + return ch, func() {} + } + timer := time.NewTimer(d) + return timer.C, func() { + if !timer.Stop() { + select { + case <-timer.C: // drain channel to unlock the resource + default: + } + } + } +} + +// TickerProxy helps enable us to switch freely between time.Ticker and clock's Ticker implementation. +type TickerProxy struct { + C <-chan time.Time + + onStop func() + onReset func(d time.Duration) +} + +func (tp *TickerProxy) Stop() { tp.onStop() } + +func (tp *TickerProxy) Reset(d time.Duration) { tp.onReset(d) } + +func NewTicker(d time.Duration) *Ticker { + ticker := &Ticker{duration: d} + ticker.init() + return ticker +} + +type Ticker struct { + C chan time.Time + + duration time.Duration + onInit sync.Once + lock sync.RWMutex // is lock really needed if only the background goroutine reads the values from it? + done chan struct{} + pulse chan struct{} + ticker *time.Ticker + lastTickedAt time.Time +} + +func (t *Ticker) init() { + t.onInit.Do(func() { + t.C = make(chan time.Time) + t.done = make(chan struct{}) + t.pulse = make(chan struct{}) + t.ticker = time.NewTicker(t.getScaledDuration()) + t.updateLastTickedAt() + go func() { + timeTravel := make(chan TimeTravelEvent) + defer Notify(timeTravel)() + + if tt, ok := Check(); ok { // trigger initial time travel awareness + if !t.handleTimeTravel(timeTravel, tt) { + return + } + } + + for { + if !t.ticking(timeTravel, t.ticker.C, tickingOption{}) { + break + } + } + }() + }) +} + +type tickingOption struct { + // OnEvent will be executed when an event is received during waiting for ticking + OnEvent func() +} + +func (h tickingOption) onEvent() { + if h.OnEvent == nil { + return + } + h.OnEvent() +} + +func (t *Ticker) ticking(timeTravel <-chan TimeTravelEvent, tick <-chan time.Time, o tickingOption) bool { + select { + case <-t.done: + o.onEvent() + return false + + case tt := <-timeTravel: // on time travel, we reset the ticker according to the new time + o.onEvent() + return t.handleTimeTravel(timeTravel, tt) + + case <-tick: // on time.Ticker tick, we also tick + o.onEvent() + select { + case tt := <-timeTravel: + return t.handleTimeTravel(timeTravel, tt) + case t.C <- t.updateLastTickedAt(): + } + return true + + } +} + +func (t *Ticker) handleTimeTravel(timeTravel <-chan TimeTravelEvent, tt TimeTravelEvent) bool { + var ( + opt = tickingOption{} + prev = tt.Prev + when = tt.When + ) + if lastTickedAt := t.getLastTickedAt(); lastTickedAt.Before(prev) { + prev = lastTickedAt + } + if fn := t.fastForwardTicksTo(prev, when); fn != nil { + opt.OnEvent = fn + } + if tt.Deep && tt.Freeze { + return t.ticking(timeTravel, nil, opt) // wait for unfreeze + } + defer t.resetTicker() + c, td := timeAfterWithCleanup(RemainingDuration(t.getLastTickedAt(), t.getRealDuration())) + defer td() + return t.ticking(timeTravel, c, opt) // wait the remaining time from the current tick +} + +func (t *Ticker) fastForwardTicksTo(from, till time.Time) func() { + var travelledDuration = till.Sub(from) + + if travelledDuration <= 0 { + return nil + } + + var ( + doneBeforeNextEvent = make(chan struct{}) + fastforwardWG = &sync.WaitGroup{} + timeBetweenTicks = t.getRealDuration() + missingTicks = int(travelledDuration / timeBetweenTicks) + ) + var OnBeforeEvent = func() { + close(doneBeforeNextEvent) + fastforwardWG.Wait() + } + + // fast forward last ticked at position to the time after the ticks + t.updateLastTickedAtTo(from.Add(timeBetweenTicks * time.Duration(missingTicks))) + + fastforwardWG.Add(1) + go func(tickedAt time.Time) { + defer fastforwardWG.Done() + + fastForward: + for i := 0; i < missingTicks; i++ { + tickedAt = tickedAt.Add(timeBetweenTicks) // move to the next tick time + select { + case <-doneBeforeNextEvent: + break fastForward + case t.C <- tickedAt: // tick! + continue fastForward + } + } + }(from) + runtime.Gosched() + + return OnBeforeEvent +} + +// Stop turns off a ticker. After Stop, no more ticks will be sent. +// Stop does not close the channel, to prevent a concurrent goroutine +// reading from the channel from seeing an erroneous "tick". +func (t *Ticker) Stop() { + t.lock.Lock() + defer t.lock.Unlock() + t.init() + close(t.done) + t.ticker.Stop() + t.onInit = sync.Once{} +} + +func (t *Ticker) Reset(d time.Duration) { + t.init() + t.setDuration(d) + t.resetTicker() +} + +func (t *Ticker) resetTicker() { + d := t.getScaledDuration() + if d == 0 { // zero is not an acceptable tick time + d = time.Nanosecond + } + t.ticker.Reset(d) +} + +// getScaledDuration returns the time duration that is altered by time +func (t *Ticker) getScaledDuration() time.Duration { + return ScaledDuration(t.getRealDuration()) +} + +func (t *Ticker) getRealDuration() time.Duration { + t.lock.RLock() + defer t.lock.RUnlock() + return t.duration +} + +func (t *Ticker) setDuration(d time.Duration) { + t.lock.Lock() + defer t.lock.Unlock() + t.duration = d +} + +func (t *Ticker) getLastTickedAt() time.Time { + t.lock.RLock() + defer t.lock.RUnlock() + return t.lastTickedAt +} + +func (t *Ticker) updateLastTickedAt() time.Time { + return t.updateLastTickedAtTo(TimeNow()) +} + +func (t *Ticker) updateLastTickedAtTo(at time.Time) time.Time { + t.lock.RLock() + defer t.lock.RUnlock() + t.lastTickedAt = at + return t.lastTickedAt +} diff --git a/clock/timecop/timecop.go b/clock/timecop/timecop.go index c66cd81..f4e916d 100644 --- a/clock/timecop/timecop.go +++ b/clock/timecop/timecop.go @@ -1,12 +1,17 @@ package timecop import ( + "runtime" "testing" "time" "go.llib.dev/testcase/clock/internal" ) +// Travel will initiate a time travel. +// It accepts either a time duration as argument to set the travel's duration, +// or a given target time if we need to travel to a specific point in time. +// It accepts optional travel options such as timecop.Freeze and timecop.DeepFreeze. func Travel[D time.Duration | time.Time](tb testing.TB, d D, tos ...TravelOption) { tb.Helper() guardAgainstParallel(tb) @@ -17,6 +22,10 @@ func Travel[D time.Duration | time.Time](tb testing.TB, d D, tos ...TravelOption case time.Time: travelByTime(tb, d, opt) } + for i, n := 0, runtime.NumGoroutine(); i < n; i++ { // since goroutines don't have guarantee when they will be scheduled + runtime.Gosched() // we explicitly mark that we are okay with other goroutines to be scheduled + time.Sleep(time.Nanosecond) // and we also okay to be low piority and blocked for the sake of other goroutines. + } } const BlazingFast = 100 diff --git a/let/std_test.go b/let/std_test.go index dda9aaa..a9bfa59 100644 --- a/let/std_test.go +++ b/let/std_test.go @@ -47,10 +47,12 @@ func TestSTD_smoke(t *testing.T) { t.Must.Error(Error.Get(t)) t.Must.NotEmpty(String.Get(t)) t.Must.NotEmpty(StringNC.Get(t)) - t.Must.True(42 == len(StringNC.Get(t))) + t.Must.True(len(StringNC.Get(t)) == 42) charsterIs(t, random.CharsetASCII(), StringNC.Get(t)) t.Must.NotEmpty(Int.Get(t)) - t.Must.NotEmpty(IntN.Get(t)) + t.Eventually(func(t *testcase.T) { + t.Must.NotEmpty(IntN.Get(testcase.ToT(&t.TB))) + }) t.Must.NotEmpty(IntB.Get(t)) t.Must.NotEmpty(Time.Get(t)) t.Must.NotEmpty(TimeB.Get(t))