quartz

package module
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 20, 2024 License: CC0-1.0 Imports: 7 Imported by: 49

README

Quartz

A Go time testing library for writing deterministic unit tests

Our high level goal is to write unit tests that

  1. execute quickly
  2. don't flake
  3. are straightforward to write and understand

For tests to execute quickly without flakes, we want to focus on determinism: the test should run the same each time, and it should be easy to force the system into a known state (no races) before executing test assertions. time.Sleep, runtime.Gosched(), and polling/Eventually are all symptoms of an inability to do this easily.

Usage

Clock interface

In your application code, maintain a reference to a quartz.Clock instance to start timers and tickers, instead of the bare time standard library.

import "github.com/coder/quartz"

type Component struct {
	...

	// for testing
	clock quartz.Clock
}

Whenever you would call into time to start a timer or ticker, call Component's clock instead.

In production, set this clock to quartz.NewReal() to create a clock that just transparently passes through to the standard time library.

Mocking

In your tests, you can use a *Mock to control the tickers and timers your code under test gets.

import (
	"testing"
	"github.com/coder/quartz"
)

func TestComponent(t *testing.T) {
	mClock := quartz.NewMock(t)
	comp := &Component{
		...
		clock: mClock,
	}
}

The *Mock clock starts at Jan 1, 2024, 00:00 UTC by default, but you can set any start time you'd like prior to your test.

mClock := quartz.NewMock(t)
mClock.Set(time.Date(2021, 6, 18, 12, 0, 0, 0, time.UTC)) // June 18, 2021 @ 12pm UTC
Advancing the clock

Once you begin setting timers or tickers, you cannot change the time backward, only advance it forward. You may continue to use Set(), but it is often easier and clearer to use Advance().

For example, with a timer:

fired := false

tmr := mClock.AfterFunc(time.Second, func() {
  fired = true
})
mClock.Advance(time.Second)

When you call Advance() it immediately moves the clock forward the given amount, and triggers any tickers or timers that are scheduled to happen at that time. Any triggered events happen on separate goroutines, so do not immediately assert the results:

fired := false

tmr := mClock.AfterFunc(time.Second, func() {
  fired = true
})
mClock.Advance(time.Second)

// RACE CONDITION, DO NOT DO THIS!
if !fired {
  t.Fatal("didn't fire")
}

Advance() (and Set() for that matter) return an AdvanceWaiter object you can use to wait for all triggered events to complete.

fired := false
// set a test timeout so we don't wait the default `go test` timeout for a failure
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

tmr := mClock.AfterFunc(time.Second, func() {
  fired = true
})

w := mClock.Advance(time.Second)
err := w.Wait(ctx)
if err != nil {
  t.Fatal("AfterFunc f never completed")
}
if !fired {
  t.Fatal("didn't fire")
}

The construction of waiting for the triggered events and failing the test if they don't complete is very common, so there is a shorthand:

w := mClock.Advance(time.Second)
err := w.Wait(ctx)
if err != nil {
  t.Fatal("AfterFunc f never completed")
}

is equivalent to:

w := mClock.Advance(time.Second)
w.MustWait(ctx)

or even more briefly:

mClock.Advance(time.Second).MustWait(ctx)
Advance only to the next event

One important restriction on advancing the clock is that you may only advance forward to the next timer or ticker event and no further. The following will result in a test failure:

func TestAdvanceTooFar(t *testing.T) {
	ctx, cancel := context.WithTimeout(10*time.Second)
	defer cancel()
	mClock := quartz.NewMock(t)
	var firedAt time.Time
	mClock.AfterFunc(time.Second, func() {
		firedAt := mClock.Now()
	})
	mClock.Advance(2*time.Second).MustWait(ctx)
}

This is a deliberate design decision to allow Advance() to immediately and synchronously move the clock forward (even without calling Wait() on returned waiter). This helps meet Quartz's design goals of writing deterministic and easy to understand unit tests. It also allows the clock to be advanced, deterministically during the execution of a tick or timer function, as explained in the next sections on Traps.

Advancing multiple events can be accomplished via looping. E.g. if you have a 1-second ticker

for i := 0; i < 10; i++ {
	mClock.Advance(time.Second).MustWait(ctx)
}

will advance 10 ticks.

If you don't know or don't want to compute the time to the next event, you can use AdvanceNext().

d, w := mClock.AdvanceNext()
w.MustWait(ctx)
// d contains the duration we advanced

d, ok := Peek() returns the duration until the next event, if any (ok is true). You can use this to advance a specific time, regardless of the tickers and timer events:

desired := time.Minute // time to advance
for desired > 0 {
	p, ok := mClock.Peek()
	if !ok || p > desired {
		mClock.Advance(desired).MustWait(ctx)
		break
	}
	mClock.Advance(p).MustWait(ctx)
	desired -= p
}
Traps

A trap allows you to match specific calls into the library while mocking, block their return, inspect their arguments, then release them to allow them to return. They help you write deterministic unit tests even when the code under test executes asynchronously from the test.

You set your traps prior to executing code under test, and then wait for them to be triggered.

func TestTrap(t *testing.T) {
	ctx, cancel := context.WithTimeout(10*time.Second)
	defer cancel()
	mClock := quartz.NewMock(t)
	trap := mClock.Trap().AfterFunc()
	defer trap.Close() // stop trapping AfterFunc calls

	count := 0
	go mClock.AfterFunc(time.Hour, func(){
		count++
	})
	call := trap.MustWait(ctx)
	call.Release()
	if call.Duration != time.Hour {
		t.Fatal("wrong duration")
	}

	// Now that the async call to AfterFunc has occurred, we can advance the clock to trigger it
	mClock.Advance(call.Duration).MustWait(ctx)
	if count != 1 {
		t.Fatal("wrong count")
	}
}

In this test, the trap serves 2 purposes. Firstly, it allows us to capture and assert the duration passed to the AfterFunc call. Secondly, it prevents a race between setting the timer and advancing it. Since these things happen on different goroutines, if Advance() completes before AfterFunc() is called, then the timer never pops in this test.

Any untrapped calls immediately complete using the current time, and calling Close() on a trap causes the mock clock to stop trapping those calls.

You may also Advance() the clock between trapping a call and releasing it. The call uses the current (mocked) time at the moment it is released.

func TestTrap2(t *testing.T) {
	ctx, cancel := context.WithTimeout(10*time.Second)
	defer cancel()
	mClock := quartz.NewMock(t)
	trap := mClock.Trap().Now()
	defer trap.Close() // stop trapping AfterFunc calls

	var logs []string
	done := make(chan struct{})
	go func(clk quartz.Clock){
		defer close(done)
		start := clk.Now()
		phase1()
		p1end := clk.Now()
		logs = append(fmt.Sprintf("Phase 1 took %s", p1end.Sub(start).String()))
		phase2()
		p2end := clk.Now()
		logs = append(fmt.Sprintf("Phase 2 took %s", p2end.Sub(p1end).String()))
	}(mClock)

	// start
	trap.MustWait(ctx).Release()
	// phase 1
	call := trap.MustWait(ctx)
	mClock.Advance(3*time.Second).MustWait(ctx)
	call.Release()
	// phase 2
	call = trap.MustWait(ctx)
	mClock.Advance(5*time.Second).MustWait(ctx)
	call.Release()

	<-done
	// Now logs contains []string{"Phase 1 took 3s", "Phase 2 took 5s"}
}
Tags

When multiple goroutines in the code under test call into the Clock, you can use tags to distinguish them in your traps.

trap := mClock.Trap.Now("foo") // traps any calls that contain "foo"
defer trap.Close()

foo := make(chan time.Time)
go func(){
	foo <- mClock.Now("foo", "bar")
}()
baz := make(chan time.Time)
go func(){
	baz <- mClock.Now("baz")
}()
call := trap.MustWait(ctx)
mClock.Advance(time.Second).MustWait(ctx)
call.Release()
// call.Tags contains []string{"foo", "bar"}

gotFoo := <-foo // 1s after start
gotBaz := <-baz // ?? never trapped, so races with Advance()

Tags appear as an optional suffix on all Clock methods (type ...string) and are ignored entirely by the real clock. They also appear on all methods on returned timers and tickers.

Options

We use the Option pattern to inject the mock clock for testing, keeping the call signature in production clean. The option pattern is compatible with other optional fields as well.

type Option func(*Thing)

// WithTestClock is used in tests to inject a mock Clock
func WithTestClock(clk quartz.Clock) Option {
	return func(t *Thing) {
		t.clock = clk
	}
}

func NewThing(<required args>, opts ...Option) *Thing {
	t := &Thing{
		...
		clock: quartz.NewReal()
	}
	for _, o := range opts {
	  o(t)
	}
	return t
}

In tests, this becomes

func TestThing(t *testing.T) {
	mClock := quartz.NewMock(t)
	thing := NewThing(<required args>, WithTestClock(mClock))
	...
}
Tagging convention

Tag your Clock method calls as:

func (c *Component) Method() {
	now := c.clock.Now("Component", "Method")
}

or

func (c *Component) Method() {
	start := c.clock.Now("Component", "Method", "start")
	...
	end := c.clock.Now("Component", "Method", "end")
}

This makes it much less likely that code changes that introduce new components or methods will spoil existing unit tests.

Why another time testing library?

Writing good unit tests for components and functions that use the time package is difficult, even though several open source libraries exist. In building Quartz, we took some inspiration from

Quartz shares the high level design of a Clock interface that closely resembles the functions in the time standard library, and a "real" clock passes thru to the standard library in production, while a mock clock gives precise control in testing.

As mentioned in our introduction, our high level goal is to write unit tests that

  1. execute quickly
  2. don't flake
  3. are straightforward to write and understand

For several reasons, this is a tall order when it comes to code that depends on time, and we found the existing libraries insufficient for our goals.

Preventing test flakes

The following example comes from the README from benbjohnson/clock:

mock := clock.NewMock()
count := 0

// Kick off a timer to increment every 1 mock second.
go func() {
	ticker := mock.Ticker(1 * time.Second)
	for {
		<-ticker.C
		count++
	}
}()
runtime.Gosched()

// Move the clock forward 10 seconds.
mock.Add(10 * time.Second)

// This prints 10.
fmt.Println(count)

The first race condition is fairly obvious: moving the clock forward 10 seconds may generate 10 ticks on the ticker.C channel, but there is no guarantee that count++ executes before fmt.Println(count).

The second race condition is more subtle, but runtime.Gosched() is the tell. Since the ticker is started on a separate goroutine, there is no guarantee that mock.Ticker() executes before mock.Add(). runtime.Gosched() is an attempt to get this to happen, but it makes no hard promises. On a busy system, especially when running tests in parallel, this can flake, advance the time 10 seconds first, then start the ticker and never generate a tick.

Let's talk about how Quartz tackles these problems.

In our experience, an extremely common use case is creating a ticker then doing a 2-arm select with ticks in one and context expiring in another, i.e.

t := time.NewTicker(duration)
for {
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-t.C:
		err := do()
		if err != nil {
			return err
		}
	}
}

In Quartz, we refactor this to be more compact and testing friendly:

t := clock.TickerFunc(ctx, duration, do)
return t.Wait()

This affords the mock Clock the ability to explicitly know when processing of a tick is finished because it's wrapped in the function passed to TickerFunc (do() in this example).

In Quartz, when you advance the clock, you are returned an object you can Wait() on to ensure all ticks and timers triggered are finished. This solves the first race condition in the example.

(As an aside, we still support a traditional standard library-style Ticker. You may find it useful if you want to keep your code as close as possible to the standard library, or if you need to use the channel in a larger select block. In that case, you'll have to find some other mechanism to sync tick processing to your test code.)

To prevent race conditions related to the starting of the ticker, Quartz allows you to set "traps" for calls that access the clock.

func TestTicker(t *testing.T) {
	mClock := quartz.NewMock(t)
	trap := mClock.Trap().TickerFunc()
	defer trap.Close() // stop trapping at end
	go runMyTicker(mClock) // async calls TickerFunc()
	call := trap.Wait(context.Background()) // waits for a call and blocks its return
	call.Release() // allow the TickerFunc() call to return
	// optionally check the duration using call.Duration
	// Move the clock forward 1 tick
	mClock.Advance(time.Second).MustWait(context.Background())
	// assert results of the tick
}

Trapping and then releasing the call to TickerFunc() ensures the ticker is started at a deterministic time, so our calls to Advance() will have a predictable effect.

Take a look at TestExampleTickerFunc in example_test.go for a complete worked example.

Complex time dependence

Another difficult issue to handle when unit testing is when some code under test makes multiple calls that depend on the time, and you want to simulate some time passing between them.

A very basic example is measuring how long something took:

var measurement time.Duration
go func(clock quartz.Clock) {
	start := clock.Now()
	doSomething()
	measurement = clock.Since(start)
}(mClock)

// how to get measurement to be, say, 5 seconds?

The two calls into the clock happen asynchronously, so we need to be able to advance the clock after the first call to Now() but before the call to Since(). Doing this with the libraries we mentioned above means that you have to be able to mock out or otherwise block the completion of doSomething().

But, with the trap functionality we mentioned in the previous section, you can deterministically control the time each call sees.

trap := mClock.Trap().Since()
var measurement time.Duration
go func(clock quartz.Clock) {
	start := clock.Now()
	doSomething()
	measurement = clock.Since(start)
}(mClock)

c := trap.Wait(ctx)
mClock.Advance(5*time.Second)
c.Release()

We wait until we trap the clock.Since() call, which implies that clock.Now() has completed, then advance the mock clock 5 seconds. Finally, we release the clock.Since() call. Any changes to the clock that happen before we release the call will be included in the time used for the clock.Since() call.

As a more involved example, consider an inactivity timeout: we want something to happen if there is no activity recorded for some period, say 10 minutes in the following example:

type InactivityTimer struct {
	mu sync.Mutex
	activity time.Time
	clock quartz.Clock
}

func (i *InactivityTimer) Start() {
	i.mu.Lock()
	defer i.mu.Unlock()
	next := i.clock.Until(i.activity.Add(10*time.Minute))
	t := i.clock.AfterFunc(next, func() {
		i.mu.Lock()
		defer i.mu.Unlock()
		next := i.clock.Until(i.activity.Add(10*time.Minute))
		if next == 0 {
			i.timeoutLocked()
			return
		}
		t.Reset(next)
	})
}

The actual contents of timeoutLocked() doesn't matter for this example, and assume there are other functions that record the latest activity.

We found that some time testing libraries hold a lock on the mock clock while calling the function passed to AfterFunc, resulting in a deadlock if you made clock calls from within.

Others allow this sort of thing, but don't have the flexibility to test edge cases. There is a subtle bug in our Start() function. The timer may pop a little late, and/or some measurable real time may elapse before Until() gets called inside the AfterFunc. If there hasn't been activity, next might be negative.

To test this in Quartz, we'll use a trap. We only want to trap the inner Until() call, not the initial one, so to make testing easier we can "tag" the call we want. Like this:

func (i *InactivityTimer) Start() {
	i.mu.Lock()
	defer i.mu.Unlock()
	next := i.clock.Until(i.activity.Add(10*time.Minute))
	t := i.clock.AfterFunc(next, func() {
		i.mu.Lock()
		defer i.mu.Unlock()
		next := i.clock.Until(i.activity.Add(10*time.Minute), "inner")
		if next == 0 {
			i.timeoutLocked()
			return
		}
		t.Reset(next)
	})
}

All Quartz Clock functions, and functions on returned timers and tickers support zero or more string tags that allow traps to match on them.

func TestInactivityTimer_Late(t *testing.T) {
	// set a timeout on the test itself, so that if Wait functions get blocked, we don't have to
	// wait for the default test timeout of 10 minutes.
	ctx, cancel := context.WithTimeout(10*time.Second)
	defer cancel()
	mClock := quartz.NewMock(t)
	trap := mClock.Trap.Until("inner")
	defer trap.Close()

	it := &InactivityTimer{
		activity: mClock.Now(),
		clock: mClock,
	}
	it.Start()

	// Trigger the AfterFunc
	w := mClock.Advance(10*time.Minute)
	c := trap.Wait(ctx)
	// Advance the clock a few ms to simulate a busy system
	mClock.Advance(3*time.Millisecond)
	c.Release() // Until() returns
	w.MustWait(ctx) // Wait for the AfterFunc to wrap up

	// Assert that the timeoutLocked() function was called
}

This test case will fail with our bugged implementation, since the triggered AfterFunc won't call timeoutLocked() and instead will reset the timer with a negative number. The fix is easy, use next <= 0 as the comparison.

Documentation

Overview

Package quartz is a library for testing time related code. It exports an interface Clock that mimics the standard library time package functions. In production, an implementation that calls thru to the standard library is used. In testing, a Mock clock is used to precisely control and intercept time functions.

Index

Constants

This section is empty.

Variables

View Source
var ErrTrapClosed = errors.New("trap closed")

Functions

This section is empty.

Types

type AdvanceWaiter

type AdvanceWaiter struct {
	// contains filtered or unexported fields
}

AdvanceWaiter is returned from Advance and Set calls and allows you to wait for ticks and timers to complete. In the case of functions passed to AfterFunc or TickerFunc, it waits for the functions to return. For other ticks & timers, it just waits for the tick to be delivered to the channel.

If multiple timers or tickers trigger simultaneously, they are all run on separate go routines.

func (AdvanceWaiter) Done

func (w AdvanceWaiter) Done() <-chan struct{}

Done returns a channel that is closed when all timers and ticks complete.

func (AdvanceWaiter) MustWait

func (w AdvanceWaiter) MustWait(ctx context.Context)

MustWait waits for all timers and ticks to complete, and fails the test immediately if the context completes first. MustWait must be called from the goroutine running the test or benchmark, similar to `t.FailNow()`.

func (AdvanceWaiter) Wait

func (w AdvanceWaiter) Wait(ctx context.Context) error

Wait for all timers and ticks to complete, or until context expires.

type Call

type Call struct {
	Time     time.Time
	Duration time.Duration
	Tags     []string
	// contains filtered or unexported fields
}

func (*Call) Release

func (c *Call) Release()

type Clock

type Clock interface {
	// 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.
	NewTicker(d time.Duration, tags ...string) *Ticker
	// TickerFunc is a convenience function that calls f on the interval d until either the given
	// context expires or f returns an error.  Callers may call Wait() on the returned Waiter to
	// wait until this happens and obtain the error. The duration d must be greater than zero; if
	// not, TickerFunc will panic.
	TickerFunc(ctx context.Context, d time.Duration, f func() error, tags ...string) Waiter
	// NewTimer creates a new Timer that will send the current time on its channel after at least
	// duration d.
	NewTimer(d time.Duration, tags ...string) *Timer
	// AfterFunc waits for the duration to elapse and then calls f in its own goroutine. It returns
	// a Timer that can be used to cancel the call using its Stop method. The returned Timer's C
	// field is not used and will be nil.
	AfterFunc(d time.Duration, f func(), tags ...string) *Timer

	// Now returns the current local time.
	Now(tags ...string) time.Time
	// Since returns the time elapsed since t. It is shorthand for Clock.Now().Sub(t).
	Since(t time.Time, tags ...string) time.Duration
	// Until returns the duration until t. It is shorthand for t.Sub(Clock.Now()).
	Until(t time.Time, tags ...string) time.Duration
}

func NewReal

func NewReal() Clock

type Mock

type Mock struct {
	// contains filtered or unexported fields
}

Mock is the testing implementation of Clock. It tracks a time that monotonically increases during a test, triggering any timers or tickers automatically.

func NewMock

func NewMock(tb testing.TB) *Mock

NewMock creates a new Mock with the time set to midnight UTC on Jan 1, 2024. You may re-set the time earlier than this, but only before timers or tickers are created.

func (*Mock) Advance

func (m *Mock) Advance(d time.Duration) AdvanceWaiter

Advance moves the clock forward by d, triggering any timers or tickers. The returned value can be used to wait for all timers and ticks to complete. Advance sets the clock forward before returning, and can only advance up to the next timer or tick event. It will fail the test if you attempt to advance beyond.

If you need to advance exactly to the next event, and don't know or don't wish to calculate it, consider AdvanceNext().

func (*Mock) AdvanceNext

func (m *Mock) AdvanceNext() (time.Duration, AdvanceWaiter)

AdvanceNext advances the clock to the next timer or tick event. It fails the test if there are none scheduled. It returns the duration the clock was advanced and a waiter that can be used to wait for the timer/tick event(s) to finish.

func (*Mock) AfterFunc

func (m *Mock) AfterFunc(d time.Duration, f func(), tags ...string) *Timer

func (*Mock) NewTicker

func (m *Mock) NewTicker(d time.Duration, tags ...string) *Ticker

func (*Mock) NewTimer

func (m *Mock) NewTimer(d time.Duration, tags ...string) *Timer

func (*Mock) Now

func (m *Mock) Now(tags ...string) time.Time

func (*Mock) Peek

func (m *Mock) Peek() (d time.Duration, ok bool)

Peek returns the duration until the next ticker or timer event and the value true, or, if there are no running tickers or timers, it returns zero and false.

func (*Mock) Set

func (m *Mock) Set(t time.Time) AdvanceWaiter

Set the time to t. If the time is after the current mocked time, then this is equivalent to Advance() with the difference. You may only Set the time earlier than the current time before starting tickers and timers (e.g. at the start of your test case).

func (*Mock) Since

func (m *Mock) Since(t time.Time, tags ...string) time.Duration

func (*Mock) TickerFunc

func (m *Mock) TickerFunc(ctx context.Context, d time.Duration, f func() error, tags ...string) Waiter

func (*Mock) Trap

func (m *Mock) Trap() Trapper

func (*Mock) Until

func (m *Mock) Until(t time.Time, tags ...string) time.Duration

type Ticker

type Ticker struct {
	C <-chan time.Time
	// contains filtered or unexported fields
}

A Ticker holds a channel that delivers “ticks” of a clock at intervals.

func (*Ticker) Reset

func (t *Ticker) Reset(d time.Duration, tags ...string)

Reset stops a ticker and resets its period to the specified duration. The next tick will arrive after the new period elapses. The duration d must be greater than zero; if not, Reset will panic.

func (*Ticker) Stop

func (t *Ticker) Stop(tags ...string)

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".

type Timer

type Timer struct {
	C <-chan time.Time
	// contains filtered or unexported fields
}

The Timer type represents a single event. When the Timer expires, the current time will be sent on C, unless the Timer was created by AfterFunc. A Timer must be created with NewTimer or AfterFunc.

func (*Timer) Reset

func (t *Timer) Reset(d time.Duration, tags ...string) bool

Reset changes the timer to expire after duration d. It returns true if the timer had been active, false if the timer had expired or been stopped.

See https://pkg.go.dev/time#Timer.Reset for more information.

func (*Timer) Stop

func (t *Timer) Stop(tags ...string) bool

Stop prevents the Timer from firing. It returns true if the call stops the timer, false if the timer has already expired or been stopped. Stop does not close the channel, to prevent a read from the channel succeeding incorrectly.

See https://pkg.go.dev/time#Timer.Stop for more information.

type Trap

type Trap struct {
	// contains filtered or unexported fields
}

func (*Trap) Close

func (t *Trap) Close()

func (*Trap) MustWait

func (t *Trap) MustWait(ctx context.Context) *Call

MustWait calls Wait() and then if there is an error, immediately fails the test via tb.Fatalf()

func (*Trap) Wait

func (t *Trap) Wait(ctx context.Context) (*Call, error)

type Trapper

type Trapper struct {
	// contains filtered or unexported fields
}

Trapper allows the creation of Traps

func (Trapper) AfterFunc

func (t Trapper) AfterFunc(tags ...string) *Trap

func (Trapper) NewTicker

func (t Trapper) NewTicker(tags ...string) *Trap

func (Trapper) NewTimer

func (t Trapper) NewTimer(tags ...string) *Trap

func (Trapper) Now

func (t Trapper) Now(tags ...string) *Trap

func (Trapper) Since

func (t Trapper) Since(tags ...string) *Trap

func (Trapper) TickerFunc

func (t Trapper) TickerFunc(tags ...string) *Trap

func (Trapper) TickerFuncWait

func (t Trapper) TickerFuncWait(tags ...string) *Trap

func (Trapper) TickerReset

func (t Trapper) TickerReset(tags ...string) *Trap

func (Trapper) TickerStop

func (t Trapper) TickerStop(tags ...string) *Trap

func (Trapper) TimerReset

func (t Trapper) TimerReset(tags ...string) *Trap

func (Trapper) TimerStop

func (t Trapper) TimerStop(tags ...string) *Trap

func (Trapper) Until

func (t Trapper) Until(tags ...string) *Trap

type Waiter

type Waiter interface {
	Wait(tags ...string) error
}

Waiter can be waited on for an error.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL