mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
chore: spin clock library out to coder/quartz repo (#13777)
Code that was in `/clock` has been moved to github.com/coder/quartz. This PR refactors our use of the clock library to point to the external Quartz repo.
This commit is contained in:
+3
-3
@@ -10,9 +10,9 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/clock"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
// PostWorkspaceAgentAppHealth updates the workspace app health.
|
||||
@@ -23,7 +23,7 @@ type WorkspaceAppHealthReporter func(ctx context.Context)
|
||||
|
||||
// NewWorkspaceAppHealthReporter creates a WorkspaceAppHealthReporter that reports app health to coderd.
|
||||
func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.WorkspaceApp, postWorkspaceAgentAppHealth PostWorkspaceAgentAppHealth) WorkspaceAppHealthReporter {
|
||||
return NewAppHealthReporterWithClock(logger, apps, postWorkspaceAgentAppHealth, clock.NewReal())
|
||||
return NewAppHealthReporterWithClock(logger, apps, postWorkspaceAgentAppHealth, quartz.NewReal())
|
||||
}
|
||||
|
||||
// NewAppHealthReporterWithClock is only called directly by test code. Product code should call
|
||||
@@ -32,7 +32,7 @@ func NewAppHealthReporterWithClock(
|
||||
logger slog.Logger,
|
||||
apps []codersdk.WorkspaceApp,
|
||||
postWorkspaceAgentAppHealth PostWorkspaceAgentAppHealth,
|
||||
clk clock.Clock,
|
||||
clk quartz.Clock,
|
||||
) WorkspaceAppHealthReporter {
|
||||
logger = logger.Named("apphealth")
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ import (
|
||||
"github.com/coder/coder/v2/agent"
|
||||
"github.com/coder/coder/v2/agent/agenttest"
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/clock"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func TestAppHealth_Healthy(t *testing.T) {
|
||||
@@ -69,7 +69,7 @@ func TestAppHealth_Healthy(t *testing.T) {
|
||||
httpapi.Write(r.Context(), w, http.StatusOK, nil)
|
||||
}),
|
||||
}
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
healthcheckTrap := mClock.Trap().TickerFunc("healthcheck")
|
||||
defer healthcheckTrap.Close()
|
||||
reportTrap := mClock.Trap().TickerFunc("report")
|
||||
@@ -137,7 +137,7 @@ func TestAppHealth_500(t *testing.T) {
|
||||
}),
|
||||
}
|
||||
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
healthcheckTrap := mClock.Trap().TickerFunc("healthcheck")
|
||||
defer healthcheckTrap.Close()
|
||||
reportTrap := mClock.Trap().TickerFunc("report")
|
||||
@@ -187,7 +187,7 @@ func TestAppHealth_Timeout(t *testing.T) {
|
||||
<-r.Context().Done()
|
||||
}),
|
||||
}
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
start := mClock.Now()
|
||||
|
||||
// for this test, it's easier to think in the number of milliseconds elapsed
|
||||
@@ -235,7 +235,7 @@ func setupAppReporter(
|
||||
ctx context.Context, t *testing.T,
|
||||
apps []codersdk.WorkspaceApp,
|
||||
handlers []http.Handler,
|
||||
clk clock.Clock,
|
||||
clk quartz.Clock,
|
||||
) (*agenttest.FakeAgentAPI, func()) {
|
||||
closers := []func(){}
|
||||
for _, app := range apps {
|
||||
|
||||
-635
@@ -1,635 +0,0 @@
|
||||
# Quartz
|
||||
|
||||
A Go time testing library for writing deterministic unit tests
|
||||
|
||||
_Note: Quartz is the name I'm targeting for the standalone open source project when we spin this
|
||||
out._
|
||||
|
||||
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](https://pkg.go.dev/github.com/stretchr/testify/assert#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.
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
w := mClock.Advance(time.Second)
|
||||
err := w.Wait(ctx)
|
||||
if err != nil {
|
||||
t.Fatal("AfterFunc f never completed")
|
||||
}
|
||||
```
|
||||
|
||||
is equivalent to:
|
||||
|
||||
```go
|
||||
w := mClock.Advance(time.Second)
|
||||
w.MustWait(ctx)
|
||||
```
|
||||
|
||||
or even more briefly:
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
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()`.
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
## Recommended Patterns
|
||||
|
||||
### 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.
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
func TestThing(t *testing.T) {
|
||||
mClock := quartz.NewMock(t)
|
||||
thing := NewThing(<required args>, WithTestClock(mClock))
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Tagging convention
|
||||
|
||||
Tag your `Clock` method calls as:
|
||||
|
||||
```go
|
||||
func (c *Component) Method() {
|
||||
now := c.clock.Now("Component", "Method")
|
||||
}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
- [github.com/benbjohnson/clock](https://github.com/benbjohnson/clock)
|
||||
- Tailscale's [tstest.Clock](https://github.com/coder/tailscale/blob/main/tstest/clock.go)
|
||||
- [github.com/aspenmesh/tock](https://github.com/aspenmesh/tock)
|
||||
|
||||
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:
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go
|
||||
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.
|
||||
@@ -1,43 +0,0 @@
|
||||
// Package clock 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.
|
||||
package clock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Waiter can be waited on for an error.
|
||||
type Waiter interface {
|
||||
Wait(tags ...string) error
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package clock_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/clock"
|
||||
)
|
||||
|
||||
type exampleTickCounter struct {
|
||||
ctx context.Context
|
||||
mu sync.Mutex
|
||||
ticks int
|
||||
clock clock.Clock
|
||||
}
|
||||
|
||||
func (c *exampleTickCounter) Ticks() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.ticks
|
||||
}
|
||||
|
||||
func (c *exampleTickCounter) count() {
|
||||
_ = c.clock.TickerFunc(c.ctx, time.Hour, func() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.ticks++
|
||||
return nil
|
||||
}, "mytag")
|
||||
}
|
||||
|
||||
func newExampleTickCounter(ctx context.Context, clk clock.Clock) *exampleTickCounter {
|
||||
tc := &exampleTickCounter{ctx: ctx, clock: clk}
|
||||
go tc.count()
|
||||
return tc
|
||||
}
|
||||
|
||||
// TestExampleTickerFunc demonstrates how to test the use of TickerFunc.
|
||||
func TestExampleTickerFunc(t *testing.T) {
|
||||
t.Parallel()
|
||||
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mClock := clock.NewMock(t)
|
||||
|
||||
// Because the ticker is started on a goroutine, we can't immediately start
|
||||
// advancing the clock, or we will race with the start of the ticker. If we
|
||||
// win that race, the clock gets advanced _before_ the ticker starts, and
|
||||
// our ticker will not get a tick.
|
||||
//
|
||||
// To handle this, we set a trap for the call to TickerFunc(), so that we
|
||||
// can assert it has been called before advancing the clock.
|
||||
trap := mClock.Trap().TickerFunc("mytag")
|
||||
defer trap.Close()
|
||||
|
||||
tc := newExampleTickCounter(ctx, mClock)
|
||||
|
||||
// Here, we wait for our trap to be triggered.
|
||||
call, err := trap.Wait(ctx)
|
||||
if err != nil {
|
||||
t.Fatal("ticker never started")
|
||||
}
|
||||
// it's good practice to release calls before any possible t.Fatal() calls
|
||||
// so that we don't leave dangling goroutines waiting for the call to be
|
||||
// released.
|
||||
call.Release()
|
||||
if call.Duration != time.Hour {
|
||||
t.Fatal("unexpected duration")
|
||||
}
|
||||
|
||||
if tks := tc.Ticks(); tks != 0 {
|
||||
t.Fatalf("expected 0 got %d ticks", tks)
|
||||
}
|
||||
|
||||
// Now that we know the ticker is started, we can advance the time.
|
||||
mClock.Advance(time.Hour).MustWait(ctx)
|
||||
|
||||
if tks := tc.Ticks(); tks != 1 {
|
||||
t.Fatalf("expected 1 got %d ticks", tks)
|
||||
}
|
||||
}
|
||||
|
||||
type exampleLatencyMeasurer struct {
|
||||
mu sync.Mutex
|
||||
lastLatency time.Duration
|
||||
}
|
||||
|
||||
func newExampleLatencyMeasurer(ctx context.Context, clk clock.Clock) *exampleLatencyMeasurer {
|
||||
m := &exampleLatencyMeasurer{}
|
||||
clk.TickerFunc(ctx, 10*time.Second, func() error {
|
||||
start := clk.Now()
|
||||
// m.doSomething()
|
||||
latency := clk.Since(start)
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.lastLatency = latency
|
||||
return nil
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *exampleLatencyMeasurer) LastLatency() time.Duration {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.lastLatency
|
||||
}
|
||||
|
||||
func TestExampleLatencyMeasurer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mClock := clock.NewMock(t)
|
||||
trap := mClock.Trap().Since()
|
||||
defer trap.Close()
|
||||
|
||||
lm := newExampleLatencyMeasurer(ctx, mClock)
|
||||
|
||||
w := mClock.Advance(10 * time.Second) // triggers first tick
|
||||
c := trap.MustWait(ctx) // call to Since()
|
||||
mClock.Advance(33 * time.Millisecond)
|
||||
c.Release()
|
||||
w.MustWait(ctx)
|
||||
|
||||
if l := lm.LastLatency(); l != 33*time.Millisecond {
|
||||
t.Fatalf("expected 33ms got %s", l.String())
|
||||
}
|
||||
|
||||
// Next tick is in 10s - 33ms, but if we don't want to calculate, we can use:
|
||||
d, w2 := mClock.AdvanceNext()
|
||||
c = trap.MustWait(ctx)
|
||||
mClock.Advance(17 * time.Millisecond)
|
||||
c.Release()
|
||||
w2.MustWait(ctx)
|
||||
|
||||
expectedD := 10*time.Second - 33*time.Millisecond
|
||||
if d != expectedD {
|
||||
t.Fatalf("expected %s got %s", expectedD.String(), d.String())
|
||||
}
|
||||
|
||||
if l := lm.LastLatency(); l != 17*time.Millisecond {
|
||||
t.Fatalf("expected 17ms got %s", l.String())
|
||||
}
|
||||
}
|
||||
-647
@@ -1,647 +0,0 @@
|
||||
package clock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Mock is the testing implementation of Clock. It tracks a time that monotonically increases
|
||||
// during a test, triggering any timers or tickers automatically.
|
||||
type Mock struct {
|
||||
tb testing.TB
|
||||
mu sync.Mutex
|
||||
|
||||
// cur is the current time
|
||||
cur time.Time
|
||||
|
||||
all []event
|
||||
nextTime time.Time
|
||||
nextEvents []event
|
||||
traps []*Trap
|
||||
}
|
||||
|
||||
type event interface {
|
||||
next() time.Time
|
||||
fire(t time.Time)
|
||||
}
|
||||
|
||||
func (m *Mock) TickerFunc(ctx context.Context, d time.Duration, f func() error, tags ...string) Waiter {
|
||||
if d <= 0 {
|
||||
panic("TickerFunc called with negative or zero duration")
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
c := newCall(clockFunctionTickerFunc, tags, withDuration(d))
|
||||
m.matchCallLocked(c)
|
||||
defer close(c.complete)
|
||||
t := &mockTickerFunc{
|
||||
ctx: ctx,
|
||||
d: d,
|
||||
f: f,
|
||||
nxt: m.cur.Add(d),
|
||||
mock: m,
|
||||
cond: sync.NewCond(&m.mu),
|
||||
}
|
||||
m.all = append(m.all, t)
|
||||
m.recomputeNextLocked()
|
||||
go t.waitForCtx()
|
||||
return t
|
||||
}
|
||||
|
||||
func (m *Mock) NewTicker(d time.Duration, tags ...string) *Ticker {
|
||||
if d <= 0 {
|
||||
panic("NewTicker called with negative or zero duration")
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
c := newCall(clockFunctionNewTicker, tags, withDuration(d))
|
||||
m.matchCallLocked(c)
|
||||
defer close(c.complete)
|
||||
// 1 element buffer follows standard library implementation
|
||||
ticks := make(chan time.Time, 1)
|
||||
t := &Ticker{
|
||||
C: ticks,
|
||||
c: ticks,
|
||||
d: d,
|
||||
nxt: m.cur.Add(d),
|
||||
mock: m,
|
||||
}
|
||||
m.addEventLocked(t)
|
||||
return t
|
||||
}
|
||||
|
||||
func (m *Mock) NewTimer(d time.Duration, tags ...string) *Timer {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
c := newCall(clockFunctionNewTimer, tags, withDuration(d))
|
||||
defer close(c.complete)
|
||||
m.matchCallLocked(c)
|
||||
ch := make(chan time.Time, 1)
|
||||
t := &Timer{
|
||||
C: ch,
|
||||
c: ch,
|
||||
nxt: m.cur.Add(d),
|
||||
mock: m,
|
||||
}
|
||||
if d <= 0 {
|
||||
// zero or negative duration timer means we should immediately fire
|
||||
// it, rather than add it.
|
||||
go t.fire(t.mock.cur)
|
||||
return t
|
||||
}
|
||||
m.addEventLocked(t)
|
||||
return t
|
||||
}
|
||||
|
||||
func (m *Mock) AfterFunc(d time.Duration, f func(), tags ...string) *Timer {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
c := newCall(clockFunctionAfterFunc, tags, withDuration(d))
|
||||
defer close(c.complete)
|
||||
m.matchCallLocked(c)
|
||||
t := &Timer{
|
||||
nxt: m.cur.Add(d),
|
||||
fn: f,
|
||||
mock: m,
|
||||
}
|
||||
if d <= 0 {
|
||||
// zero or negative duration timer means we should immediately fire
|
||||
// it, rather than add it.
|
||||
go t.fire(t.mock.cur)
|
||||
return t
|
||||
}
|
||||
m.addEventLocked(t)
|
||||
return t
|
||||
}
|
||||
|
||||
func (m *Mock) Now(tags ...string) time.Time {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
c := newCall(clockFunctionNow, tags)
|
||||
defer close(c.complete)
|
||||
m.matchCallLocked(c)
|
||||
return m.cur
|
||||
}
|
||||
|
||||
func (m *Mock) Since(t time.Time, tags ...string) time.Duration {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
c := newCall(clockFunctionSince, tags, withTime(t))
|
||||
defer close(c.complete)
|
||||
m.matchCallLocked(c)
|
||||
return m.cur.Sub(t)
|
||||
}
|
||||
|
||||
func (m *Mock) Until(t time.Time, tags ...string) time.Duration {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
c := newCall(clockFunctionUntil, tags, withTime(t))
|
||||
defer close(c.complete)
|
||||
m.matchCallLocked(c)
|
||||
return t.Sub(m.cur)
|
||||
}
|
||||
|
||||
func (m *Mock) addEventLocked(e event) {
|
||||
m.all = append(m.all, e)
|
||||
m.recomputeNextLocked()
|
||||
}
|
||||
|
||||
func (m *Mock) recomputeNextLocked() {
|
||||
var best time.Time
|
||||
var events []event
|
||||
for _, e := range m.all {
|
||||
if best.IsZero() || e.next().Before(best) {
|
||||
best = e.next()
|
||||
events = []event{e}
|
||||
continue
|
||||
}
|
||||
if e.next().Equal(best) {
|
||||
events = append(events, e)
|
||||
continue
|
||||
}
|
||||
}
|
||||
m.nextTime = best
|
||||
m.nextEvents = events
|
||||
}
|
||||
|
||||
func (m *Mock) removeTimer(t *Timer) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.removeTimerLocked(t)
|
||||
}
|
||||
|
||||
func (m *Mock) removeTimerLocked(t *Timer) {
|
||||
t.stopped = true
|
||||
m.removeEventLocked(t)
|
||||
}
|
||||
|
||||
func (m *Mock) removeEventLocked(e event) {
|
||||
defer m.recomputeNextLocked()
|
||||
for i := range m.all {
|
||||
if m.all[i] == e {
|
||||
m.all = append(m.all[:i], m.all[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mock) matchCallLocked(c *Call) {
|
||||
var traps []*Trap
|
||||
for _, t := range m.traps {
|
||||
if t.matches(c) {
|
||||
traps = append(traps, t)
|
||||
}
|
||||
}
|
||||
if len(traps) == 0 {
|
||||
return
|
||||
}
|
||||
c.releases.Add(len(traps))
|
||||
m.mu.Unlock()
|
||||
for _, t := range traps {
|
||||
go t.catch(c)
|
||||
}
|
||||
c.releases.Wait()
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
||||
// 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.
|
||||
type AdvanceWaiter struct {
|
||||
tb testing.TB
|
||||
ch chan struct{}
|
||||
}
|
||||
|
||||
// Wait for all timers and ticks to complete, or until context expires.
|
||||
func (w AdvanceWaiter) Wait(ctx context.Context) error {
|
||||
select {
|
||||
case <-w.ch:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (w AdvanceWaiter) MustWait(ctx context.Context) {
|
||||
w.tb.Helper()
|
||||
select {
|
||||
case <-w.ch:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
w.tb.Fatalf("context expired while waiting for clock to advance: %s", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when all timers and ticks complete.
|
||||
func (w AdvanceWaiter) Done() <-chan struct{} {
|
||||
return w.ch
|
||||
}
|
||||
|
||||
// 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 (m *Mock) Advance(d time.Duration) AdvanceWaiter {
|
||||
m.tb.Helper()
|
||||
w := AdvanceWaiter{tb: m.tb, ch: make(chan struct{})}
|
||||
m.mu.Lock()
|
||||
fin := m.cur.Add(d)
|
||||
// nextTime.IsZero implies no events scheduled.
|
||||
if m.nextTime.IsZero() || fin.Before(m.nextTime) {
|
||||
m.cur = fin
|
||||
m.mu.Unlock()
|
||||
close(w.ch)
|
||||
return w
|
||||
}
|
||||
if fin.After(m.nextTime) {
|
||||
m.tb.Errorf(fmt.Sprintf("cannot advance %s which is beyond next timer/ticker event in %s",
|
||||
d.String(), m.nextTime.Sub(m.cur)))
|
||||
m.mu.Unlock()
|
||||
close(w.ch)
|
||||
return w
|
||||
}
|
||||
|
||||
m.cur = m.nextTime
|
||||
go m.advanceLocked(w)
|
||||
return w
|
||||
}
|
||||
|
||||
func (m *Mock) advanceLocked(w AdvanceWaiter) {
|
||||
defer close(w.ch)
|
||||
wg := sync.WaitGroup{}
|
||||
for i := range m.nextEvents {
|
||||
e := m.nextEvents[i]
|
||||
t := m.cur
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
e.fire(t)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
// release the lock and let the events resolve. This allows them to call back into the
|
||||
// Mock to query the time or set new timers. Each event should remove or reschedule
|
||||
// itself from nextEvents.
|
||||
m.mu.Unlock()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 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 (m *Mock) Set(t time.Time) AdvanceWaiter {
|
||||
m.tb.Helper()
|
||||
w := AdvanceWaiter{tb: m.tb, ch: make(chan struct{})}
|
||||
m.mu.Lock()
|
||||
if t.Before(m.cur) {
|
||||
defer close(w.ch)
|
||||
defer m.mu.Unlock()
|
||||
// past
|
||||
if !m.nextTime.IsZero() {
|
||||
m.tb.Error("Set mock clock to the past after timers/tickers started")
|
||||
}
|
||||
m.cur = t
|
||||
return w
|
||||
}
|
||||
// future
|
||||
// nextTime.IsZero implies no events scheduled.
|
||||
if m.nextTime.IsZero() || t.Before(m.nextTime) {
|
||||
defer close(w.ch)
|
||||
defer m.mu.Unlock()
|
||||
m.cur = t
|
||||
return w
|
||||
}
|
||||
if t.After(m.nextTime) {
|
||||
defer close(w.ch)
|
||||
defer m.mu.Unlock()
|
||||
m.tb.Errorf("cannot Set time to %s which is beyond next timer/ticker event at %s",
|
||||
t.String(), m.nextTime)
|
||||
return w
|
||||
}
|
||||
|
||||
m.cur = m.nextTime
|
||||
go m.advanceLocked(w)
|
||||
return w
|
||||
}
|
||||
|
||||
// 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 (m *Mock) AdvanceNext() (time.Duration, AdvanceWaiter) {
|
||||
m.mu.Lock()
|
||||
m.tb.Helper()
|
||||
w := AdvanceWaiter{tb: m.tb, ch: make(chan struct{})}
|
||||
if m.nextTime.IsZero() {
|
||||
defer close(w.ch)
|
||||
defer m.mu.Unlock()
|
||||
m.tb.Error("cannot AdvanceNext because there are no timers or tickers running")
|
||||
}
|
||||
d := m.nextTime.Sub(m.cur)
|
||||
m.cur = m.nextTime
|
||||
go m.advanceLocked(w)
|
||||
return d, w
|
||||
}
|
||||
|
||||
// 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 (m *Mock) Peek() (d time.Duration, ok bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.nextTime.IsZero() {
|
||||
return 0, false
|
||||
}
|
||||
return m.nextTime.Sub(m.cur), true
|
||||
}
|
||||
|
||||
// Trapper allows the creation of Traps
|
||||
type Trapper struct {
|
||||
// mock is the underlying Mock. This is a thin wrapper around Mock so that
|
||||
// we can have our interface look like mClock.Trap().NewTimer("foo")
|
||||
mock *Mock
|
||||
}
|
||||
|
||||
func (t Trapper) NewTimer(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionNewTimer, tags)
|
||||
}
|
||||
|
||||
func (t Trapper) AfterFunc(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionAfterFunc, tags)
|
||||
}
|
||||
|
||||
func (t Trapper) TimerStop(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionTimerStop, tags)
|
||||
}
|
||||
|
||||
func (t Trapper) TimerReset(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionTimerReset, tags)
|
||||
}
|
||||
|
||||
func (t Trapper) TickerFunc(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionTickerFunc, tags)
|
||||
}
|
||||
|
||||
func (t Trapper) TickerFuncWait(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionTickerFuncWait, tags)
|
||||
}
|
||||
|
||||
func (t Trapper) NewTicker(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionNewTicker, tags)
|
||||
}
|
||||
|
||||
func (t Trapper) TickerStop(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionTickerStop, tags)
|
||||
}
|
||||
|
||||
func (t Trapper) TickerReset(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionTickerReset, tags)
|
||||
}
|
||||
|
||||
func (t Trapper) Now(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionNow, tags)
|
||||
}
|
||||
|
||||
func (t Trapper) Since(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionSince, tags)
|
||||
}
|
||||
|
||||
func (t Trapper) Until(tags ...string) *Trap {
|
||||
return t.mock.newTrap(clockFunctionUntil, tags)
|
||||
}
|
||||
|
||||
func (m *Mock) Trap() Trapper {
|
||||
return Trapper{m}
|
||||
}
|
||||
|
||||
func (m *Mock) newTrap(fn clockFunction, tags []string) *Trap {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
tr := &Trap{
|
||||
fn: fn,
|
||||
tags: tags,
|
||||
mock: m,
|
||||
calls: make(chan *Call),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
m.traps = append(m.traps, tr)
|
||||
return tr
|
||||
}
|
||||
|
||||
// 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 NewMock(tb testing.TB) *Mock {
|
||||
cur, err := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &Mock{
|
||||
tb: tb,
|
||||
cur: cur,
|
||||
}
|
||||
}
|
||||
|
||||
var _ Clock = &Mock{}
|
||||
|
||||
type mockTickerFunc struct {
|
||||
ctx context.Context
|
||||
d time.Duration
|
||||
f func() error
|
||||
nxt time.Time
|
||||
mock *Mock
|
||||
|
||||
// cond is a condition Locked on the main Mock.mu
|
||||
cond *sync.Cond
|
||||
// done is true when the ticker exits
|
||||
done bool
|
||||
// err holds the error when the ticker exits
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockTickerFunc) next() time.Time {
|
||||
return m.nxt
|
||||
}
|
||||
|
||||
func (m *mockTickerFunc) fire(_ time.Time) {
|
||||
m.mock.mu.Lock()
|
||||
defer m.mock.mu.Unlock()
|
||||
if m.done {
|
||||
return
|
||||
}
|
||||
m.nxt = m.nxt.Add(m.d)
|
||||
m.mock.recomputeNextLocked()
|
||||
|
||||
m.mock.mu.Unlock()
|
||||
err := m.f()
|
||||
m.mock.mu.Lock()
|
||||
if err != nil {
|
||||
m.exitLocked(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockTickerFunc) exitLocked(err error) {
|
||||
if m.done {
|
||||
return
|
||||
}
|
||||
m.done = true
|
||||
m.err = err
|
||||
m.mock.removeEventLocked(m)
|
||||
m.cond.Broadcast()
|
||||
}
|
||||
|
||||
func (m *mockTickerFunc) waitForCtx() {
|
||||
<-m.ctx.Done()
|
||||
m.mock.mu.Lock()
|
||||
defer m.mock.mu.Unlock()
|
||||
m.exitLocked(m.ctx.Err())
|
||||
}
|
||||
|
||||
func (m *mockTickerFunc) Wait(tags ...string) error {
|
||||
m.mock.mu.Lock()
|
||||
defer m.mock.mu.Unlock()
|
||||
c := newCall(clockFunctionTickerFuncWait, tags)
|
||||
m.mock.matchCallLocked(c)
|
||||
defer close(c.complete)
|
||||
for !m.done {
|
||||
m.cond.Wait()
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
var _ Waiter = &mockTickerFunc{}
|
||||
|
||||
type clockFunction int
|
||||
|
||||
const (
|
||||
clockFunctionNewTimer clockFunction = iota
|
||||
clockFunctionAfterFunc
|
||||
clockFunctionTimerStop
|
||||
clockFunctionTimerReset
|
||||
clockFunctionTickerFunc
|
||||
clockFunctionTickerFuncWait
|
||||
clockFunctionNewTicker
|
||||
clockFunctionTickerReset
|
||||
clockFunctionTickerStop
|
||||
clockFunctionNow
|
||||
clockFunctionSince
|
||||
clockFunctionUntil
|
||||
)
|
||||
|
||||
type callArg func(c *Call)
|
||||
|
||||
type Call struct {
|
||||
Time time.Time
|
||||
Duration time.Duration
|
||||
Tags []string
|
||||
|
||||
fn clockFunction
|
||||
releases sync.WaitGroup
|
||||
complete chan struct{}
|
||||
}
|
||||
|
||||
func (c *Call) Release() {
|
||||
c.releases.Done()
|
||||
<-c.complete
|
||||
}
|
||||
|
||||
func withTime(t time.Time) callArg {
|
||||
return func(c *Call) {
|
||||
c.Time = t
|
||||
}
|
||||
}
|
||||
|
||||
func withDuration(d time.Duration) callArg {
|
||||
return func(c *Call) {
|
||||
c.Duration = d
|
||||
}
|
||||
}
|
||||
|
||||
func newCall(fn clockFunction, tags []string, args ...callArg) *Call {
|
||||
c := &Call{
|
||||
fn: fn,
|
||||
Tags: tags,
|
||||
complete: make(chan struct{}),
|
||||
}
|
||||
for _, a := range args {
|
||||
a(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type Trap struct {
|
||||
fn clockFunction
|
||||
tags []string
|
||||
mock *Mock
|
||||
calls chan *Call
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (t *Trap) catch(c *Call) {
|
||||
select {
|
||||
case t.calls <- c:
|
||||
case <-t.done:
|
||||
c.Release()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Trap) matches(c *Call) bool {
|
||||
if t.fn != c.fn {
|
||||
return false
|
||||
}
|
||||
for _, tag := range t.tags {
|
||||
if !slices.Contains(c.Tags, tag) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Trap) Close() {
|
||||
t.mock.mu.Lock()
|
||||
defer t.mock.mu.Unlock()
|
||||
for i, tr := range t.mock.traps {
|
||||
if t == tr {
|
||||
t.mock.traps = append(t.mock.traps[:i], t.mock.traps[i+1:]...)
|
||||
}
|
||||
}
|
||||
close(t.done)
|
||||
}
|
||||
|
||||
var ErrTrapClosed = xerrors.New("trap closed")
|
||||
|
||||
func (t *Trap) Wait(ctx context.Context) (*Call, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-t.done:
|
||||
return nil, ErrTrapClosed
|
||||
case c := <-t.calls:
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
// MustWait calls Wait() and then if there is an error, immediately fails the
|
||||
// test via tb.Fatalf()
|
||||
func (t *Trap) MustWait(ctx context.Context) *Call {
|
||||
t.mock.tb.Helper()
|
||||
c, err := t.Wait(ctx)
|
||||
if err != nil {
|
||||
t.mock.tb.Fatalf("context expired while waiting for trap: %s", err.Error())
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
package clock_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/clock"
|
||||
)
|
||||
|
||||
func TestTimer_NegativeDuration(t *testing.T) {
|
||||
t.Parallel()
|
||||
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mClock := clock.NewMock(t)
|
||||
start := mClock.Now()
|
||||
trap := mClock.Trap().NewTimer()
|
||||
defer trap.Close()
|
||||
|
||||
timers := make(chan *clock.Timer, 1)
|
||||
go func() {
|
||||
timers <- mClock.NewTimer(-time.Second)
|
||||
}()
|
||||
c := trap.MustWait(ctx)
|
||||
c.Release()
|
||||
// trap returns the actual passed value
|
||||
if c.Duration != -time.Second {
|
||||
t.Fatalf("expected -time.Second, got: %v", c.Duration)
|
||||
}
|
||||
|
||||
tmr := <-timers
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for timer")
|
||||
case tme := <-tmr.C:
|
||||
// the tick is the current time, not the past
|
||||
if !tme.Equal(start) {
|
||||
t.Fatalf("expected time %v, got %v", start, tme)
|
||||
}
|
||||
}
|
||||
if tmr.Stop() {
|
||||
t.Fatal("timer still running")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAfterFunc_NegativeDuration(t *testing.T) {
|
||||
t.Parallel()
|
||||
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mClock := clock.NewMock(t)
|
||||
trap := mClock.Trap().AfterFunc()
|
||||
defer trap.Close()
|
||||
|
||||
timers := make(chan *clock.Timer, 1)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
timers <- mClock.AfterFunc(-time.Second, func() {
|
||||
close(done)
|
||||
})
|
||||
}()
|
||||
c := trap.MustWait(ctx)
|
||||
c.Release()
|
||||
// trap returns the actual passed value
|
||||
if c.Duration != -time.Second {
|
||||
t.Fatalf("expected -time.Second, got: %v", c.Duration)
|
||||
}
|
||||
|
||||
tmr := <-timers
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for timer")
|
||||
case <-done:
|
||||
// OK!
|
||||
}
|
||||
if tmr.Stop() {
|
||||
t.Fatal("timer still running")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTicker(t *testing.T) {
|
||||
t.Parallel()
|
||||
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mClock := clock.NewMock(t)
|
||||
start := mClock.Now()
|
||||
trapNT := mClock.Trap().NewTicker("new")
|
||||
defer trapNT.Close()
|
||||
trapStop := mClock.Trap().TickerStop("stop")
|
||||
defer trapStop.Close()
|
||||
trapReset := mClock.Trap().TickerReset("reset")
|
||||
defer trapReset.Close()
|
||||
|
||||
tickers := make(chan *clock.Ticker, 1)
|
||||
go func() {
|
||||
tickers <- mClock.NewTicker(time.Hour, "new")
|
||||
}()
|
||||
c := trapNT.MustWait(ctx)
|
||||
c.Release()
|
||||
if c.Duration != time.Hour {
|
||||
t.Fatalf("expected time.Hour, got: %v", c.Duration)
|
||||
}
|
||||
tkr := <-tickers
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
mClock.Advance(time.Hour).MustWait(ctx)
|
||||
}
|
||||
|
||||
// should get first tick, rest dropped
|
||||
tTime := start.Add(time.Hour)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for ticker")
|
||||
case tick := <-tkr.C:
|
||||
if !tick.Equal(tTime) {
|
||||
t.Fatalf("expected time %v, got %v", tTime, tick)
|
||||
}
|
||||
}
|
||||
|
||||
go tkr.Reset(time.Minute, "reset")
|
||||
c = trapReset.MustWait(ctx)
|
||||
mClock.Advance(time.Second).MustWait(ctx)
|
||||
c.Release()
|
||||
if c.Duration != time.Minute {
|
||||
t.Fatalf("expected time.Minute, got: %v", c.Duration)
|
||||
}
|
||||
mClock.Advance(time.Minute).MustWait(ctx)
|
||||
|
||||
// tick should show present time, ensuring the 2 hour ticks got dropped when
|
||||
// we didn't read from the channel.
|
||||
tTime = mClock.Now()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for ticker")
|
||||
case tick := <-tkr.C:
|
||||
if !tick.Equal(tTime) {
|
||||
t.Fatalf("expected time %v, got %v", tTime, tick)
|
||||
}
|
||||
}
|
||||
|
||||
go tkr.Stop("stop")
|
||||
trapStop.MustWait(ctx).Release()
|
||||
mClock.Advance(time.Hour).MustWait(ctx)
|
||||
select {
|
||||
case <-tkr.C:
|
||||
t.Fatal("ticker still running")
|
||||
default:
|
||||
// OK
|
||||
}
|
||||
|
||||
// Resetting after stop
|
||||
go tkr.Reset(time.Minute, "reset")
|
||||
trapReset.MustWait(ctx).Release()
|
||||
mClock.Advance(time.Minute).MustWait(ctx)
|
||||
tTime = mClock.Now()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for ticker")
|
||||
case tick := <-tkr.C:
|
||||
if !tick.Equal(tTime) {
|
||||
t.Fatalf("expected time %v, got %v", tTime, tick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeek(t *testing.T) {
|
||||
t.Parallel()
|
||||
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mClock := clock.NewMock(t)
|
||||
d, ok := mClock.Peek()
|
||||
if d != 0 {
|
||||
t.Fatal("expected Peek() to return 0")
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("expected Peek() to return false")
|
||||
}
|
||||
|
||||
tmr := mClock.NewTimer(time.Second)
|
||||
d, ok = mClock.Peek()
|
||||
if d != time.Second {
|
||||
t.Fatal("expected Peek() to return 1s")
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("expected Peek() to return true")
|
||||
}
|
||||
|
||||
mClock.Advance(999 * time.Millisecond).MustWait(ctx)
|
||||
d, ok = mClock.Peek()
|
||||
if d != time.Millisecond {
|
||||
t.Fatal("expected Peek() to return 1ms")
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("expected Peek() to return true")
|
||||
}
|
||||
|
||||
stopped := tmr.Stop()
|
||||
if !stopped {
|
||||
t.Fatal("expected Stop() to return true")
|
||||
}
|
||||
|
||||
d, ok = mClock.Peek()
|
||||
if d != 0 {
|
||||
t.Fatal("expected Peek() to return 0")
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("expected Peek() to return false")
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package clock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type realClock struct{}
|
||||
|
||||
func NewReal() Clock {
|
||||
return realClock{}
|
||||
}
|
||||
|
||||
func (realClock) NewTicker(d time.Duration, _ ...string) *Ticker {
|
||||
tkr := time.NewTicker(d)
|
||||
return &Ticker{ticker: tkr, C: tkr.C}
|
||||
}
|
||||
|
||||
func (realClock) TickerFunc(ctx context.Context, d time.Duration, f func() error, _ ...string) Waiter {
|
||||
ct := &realContextTicker{
|
||||
ctx: ctx,
|
||||
tkr: time.NewTicker(d),
|
||||
f: f,
|
||||
err: make(chan error, 1),
|
||||
}
|
||||
go ct.run()
|
||||
return ct
|
||||
}
|
||||
|
||||
type realContextTicker struct {
|
||||
ctx context.Context
|
||||
tkr *time.Ticker
|
||||
f func() error
|
||||
err chan error
|
||||
}
|
||||
|
||||
func (t *realContextTicker) Wait(_ ...string) error {
|
||||
return <-t.err
|
||||
}
|
||||
|
||||
func (t *realContextTicker) run() {
|
||||
defer t.tkr.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-t.ctx.Done():
|
||||
t.err <- t.ctx.Err()
|
||||
return
|
||||
case <-t.tkr.C:
|
||||
err := t.f()
|
||||
if err != nil {
|
||||
t.err <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (realClock) NewTimer(d time.Duration, _ ...string) *Timer {
|
||||
rt := time.NewTimer(d)
|
||||
return &Timer{C: rt.C, timer: rt}
|
||||
}
|
||||
|
||||
func (realClock) AfterFunc(d time.Duration, f func(), _ ...string) *Timer {
|
||||
rt := time.AfterFunc(d, f)
|
||||
return &Timer{C: rt.C, timer: rt}
|
||||
}
|
||||
|
||||
func (realClock) Now(_ ...string) time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (realClock) Since(t time.Time, _ ...string) time.Duration {
|
||||
return time.Since(t)
|
||||
}
|
||||
|
||||
func (realClock) Until(t time.Time, _ ...string) time.Duration {
|
||||
return time.Until(t)
|
||||
}
|
||||
|
||||
var _ Clock = realClock{}
|
||||
@@ -1,75 +0,0 @@
|
||||
package clock
|
||||
|
||||
import "time"
|
||||
|
||||
// A Ticker holds a channel that delivers “ticks” of a clock at intervals.
|
||||
type Ticker struct {
|
||||
C <-chan time.Time
|
||||
//nolint: revive
|
||||
c chan time.Time
|
||||
ticker *time.Ticker // realtime impl, if set
|
||||
d time.Duration // period, if set
|
||||
nxt time.Time // next tick time
|
||||
mock *Mock // mock clock, if set
|
||||
stopped bool // true if the ticker is not running
|
||||
}
|
||||
|
||||
func (t *Ticker) fire(tt time.Time) {
|
||||
t.mock.mu.Lock()
|
||||
defer t.mock.mu.Unlock()
|
||||
if t.stopped {
|
||||
return
|
||||
}
|
||||
for !t.nxt.After(t.mock.cur) {
|
||||
t.nxt = t.nxt.Add(t.d)
|
||||
}
|
||||
t.mock.recomputeNextLocked()
|
||||
select {
|
||||
case t.c <- tt:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Ticker) next() time.Time {
|
||||
return t.nxt
|
||||
}
|
||||
|
||||
// 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(tags ...string) {
|
||||
if t.ticker != nil {
|
||||
t.ticker.Stop()
|
||||
return
|
||||
}
|
||||
t.mock.mu.Lock()
|
||||
defer t.mock.mu.Unlock()
|
||||
c := newCall(clockFunctionTickerStop, tags)
|
||||
t.mock.matchCallLocked(c)
|
||||
defer close(c.complete)
|
||||
t.mock.removeEventLocked(t)
|
||||
t.stopped = true
|
||||
}
|
||||
|
||||
// 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 (t *Ticker) Reset(d time.Duration, tags ...string) {
|
||||
if t.ticker != nil {
|
||||
t.ticker.Reset(d)
|
||||
return
|
||||
}
|
||||
t.mock.mu.Lock()
|
||||
defer t.mock.mu.Unlock()
|
||||
c := newCall(clockFunctionTickerReset, tags, withDuration(d))
|
||||
t.mock.matchCallLocked(c)
|
||||
defer close(c.complete)
|
||||
t.nxt = t.mock.cur.Add(d)
|
||||
t.d = d
|
||||
if t.stopped {
|
||||
t.stopped = false
|
||||
t.mock.addEventLocked(t)
|
||||
} else {
|
||||
t.mock.recomputeNextLocked()
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package clock
|
||||
|
||||
import "time"
|
||||
|
||||
// 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.
|
||||
type Timer struct {
|
||||
C <-chan time.Time
|
||||
//nolint: revive
|
||||
c chan time.Time
|
||||
timer *time.Timer // realtime impl, if set
|
||||
nxt time.Time // next tick time
|
||||
mock *Mock // mock clock, if set
|
||||
fn func() // AfterFunc function, if set
|
||||
stopped bool // True if stopped, false if running
|
||||
}
|
||||
|
||||
func (t *Timer) fire(tt time.Time) {
|
||||
t.mock.removeTimer(t)
|
||||
if t.fn != nil {
|
||||
t.fn()
|
||||
} else {
|
||||
t.c <- tt
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Timer) next() time.Time {
|
||||
return t.nxt
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (t *Timer) Stop(tags ...string) bool {
|
||||
if t.timer != nil {
|
||||
return t.timer.Stop()
|
||||
}
|
||||
t.mock.mu.Lock()
|
||||
defer t.mock.mu.Unlock()
|
||||
c := newCall(clockFunctionTimerStop, tags)
|
||||
t.mock.matchCallLocked(c)
|
||||
defer close(c.complete)
|
||||
result := !t.stopped
|
||||
t.mock.removeTimerLocked(t)
|
||||
return result
|
||||
}
|
||||
|
||||
// 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 (t *Timer) Reset(d time.Duration, tags ...string) bool {
|
||||
if t.timer != nil {
|
||||
return t.timer.Reset(d)
|
||||
}
|
||||
t.mock.mu.Lock()
|
||||
defer t.mock.mu.Unlock()
|
||||
c := newCall(clockFunctionTimerReset, tags, withDuration(d))
|
||||
t.mock.matchCallLocked(c)
|
||||
defer close(c.complete)
|
||||
result := !t.stopped
|
||||
select {
|
||||
case <-t.c:
|
||||
default:
|
||||
}
|
||||
if d <= 0 {
|
||||
// zero or negative duration timer means we should immediately re-fire
|
||||
// it, rather than remove and re-add it.
|
||||
t.stopped = false
|
||||
go t.fire(t.mock.cur)
|
||||
return result
|
||||
}
|
||||
t.mock.removeTimerLocked(t)
|
||||
t.stopped = false
|
||||
t.nxt = t.mock.cur.Add(d)
|
||||
t.mock.addEventLocked(t)
|
||||
return result
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/clock"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
// Notifier triggers callbacks at given intervals until some event happens. The
|
||||
@@ -26,7 +26,7 @@ type Notifier struct {
|
||||
countdown []time.Duration
|
||||
|
||||
// for testing
|
||||
clock clock.Clock
|
||||
clock quartz.Clock
|
||||
}
|
||||
|
||||
// Condition is a function that gets executed periodically, and receives the
|
||||
@@ -43,7 +43,7 @@ type Condition func(now time.Time) (deadline time.Time, callback func())
|
||||
type Option func(*Notifier)
|
||||
|
||||
// WithTestClock is used in tests to inject a mock Clock
|
||||
func WithTestClock(clk clock.Clock) Option {
|
||||
func WithTestClock(clk quartz.Clock) Option {
|
||||
return func(n *Notifier) {
|
||||
n.clock = clk
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func New(cond Condition, interval time.Duration, countdown []time.Duration, opts
|
||||
countdown: ct,
|
||||
condition: cond,
|
||||
notifiedAt: make(map[time.Duration]bool),
|
||||
clock: clock.NewReal(),
|
||||
clock: quartz.NewReal(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(n)
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"github.com/coder/coder/v2/clock"
|
||||
"github.com/coder/coder/v2/coderd/autobuild/notify"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func TestNotifier(t *testing.T) {
|
||||
@@ -87,7 +87,7 @@ func TestNotifier(t *testing.T) {
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
mClock.Set(now).MustWait(ctx)
|
||||
numConditions := 0
|
||||
numCalls := 0
|
||||
|
||||
+2
-2
@@ -39,7 +39,6 @@ import (
|
||||
"cdr.dev/slog"
|
||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/clock"
|
||||
_ "github.com/coder/coder/v2/coderd/apidoc" // Used for swagger docs.
|
||||
"github.com/coder/coder/v2/coderd/appearance"
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
@@ -76,6 +75,7 @@ import (
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
"github.com/coder/coder/v2/site"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
"github.com/coder/quartz"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
@@ -573,7 +573,7 @@ func New(options *Options) *API {
|
||||
options.PrometheusRegistry.MustRegister(stn)
|
||||
}
|
||||
api.NetworkTelemetryBatcher = tailnet.NewNetworkTelemetryBatcher(
|
||||
clock.NewReal(),
|
||||
quartz.NewReal(),
|
||||
api.Options.NetworkTelemetryBatchFrequency,
|
||||
api.Options.NetworkTelemetryBatchMaxSize,
|
||||
api.handleNetworkTelemetry,
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/clock"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,15 +31,15 @@ type Watchdog struct {
|
||||
timeout chan struct{}
|
||||
|
||||
// for testing
|
||||
clock clock.Clock
|
||||
clock quartz.Clock
|
||||
}
|
||||
|
||||
func NewWatchdog(ctx context.Context, logger slog.Logger, ps Pubsub) *Watchdog {
|
||||
return NewWatchdogWithClock(ctx, logger, ps, clock.NewReal())
|
||||
return NewWatchdogWithClock(ctx, logger, ps, quartz.NewReal())
|
||||
}
|
||||
|
||||
// NewWatchdogWithClock returns a watchdog with the given clock. Product code should always call NewWatchDog.
|
||||
func NewWatchdogWithClock(ctx context.Context, logger slog.Logger, ps Pubsub, c clock.Clock) *Watchdog {
|
||||
func NewWatchdogWithClock(ctx context.Context, logger slog.Logger, ps Pubsub, c quartz.Clock) *Watchdog {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
w := &Watchdog{
|
||||
ctx: ctx,
|
||||
|
||||
@@ -8,15 +8,15 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/clock"
|
||||
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func TestWatchdog_NoTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
fPS := newFakePubsub()
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestWatchdog_NoTimeout(t *testing.T) {
|
||||
func TestWatchdog_Timeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
fPS := newFakePubsub()
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
gProto "google.golang.org/protobuf/proto"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/clock"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||||
@@ -23,6 +22,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
agpl "github.com/coder/coder/v2/tailnet"
|
||||
"github.com/coder/coder/v2/tailnet/proto"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -116,16 +116,16 @@ var pgCoordSubject = rbac.Subject{
|
||||
// NewPGCoord creates a high-availability coordinator that stores state in the PostgreSQL database and
|
||||
// receives notifications of updates via the pubsub.
|
||||
func NewPGCoord(ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, store database.Store) (agpl.Coordinator, error) {
|
||||
return newPGCoordInternal(ctx, logger, ps, store, clock.NewReal())
|
||||
return newPGCoordInternal(ctx, logger, ps, store, quartz.NewReal())
|
||||
}
|
||||
|
||||
// NewTestPGCoord is only used in testing to pass a clock.Clock in.
|
||||
func NewTestPGCoord(ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, store database.Store, clk clock.Clock) (agpl.Coordinator, error) {
|
||||
func NewTestPGCoord(ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, store database.Store, clk quartz.Clock) (agpl.Coordinator, error) {
|
||||
return newPGCoordInternal(ctx, logger, ps, store, clk)
|
||||
}
|
||||
|
||||
func newPGCoordInternal(
|
||||
ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, store database.Store, clk clock.Clock,
|
||||
ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, store database.Store, clk quartz.Clock,
|
||||
) (
|
||||
*pgCoord, error,
|
||||
) {
|
||||
@@ -823,7 +823,7 @@ func newQuerier(ctx context.Context,
|
||||
closeConnections chan *connIO,
|
||||
numWorkers int,
|
||||
firstHeartbeat chan struct{},
|
||||
clk clock.Clock,
|
||||
clk quartz.Clock,
|
||||
) *querier {
|
||||
updates := make(chan hbUpdate)
|
||||
q := &querier{
|
||||
@@ -1469,12 +1469,12 @@ type heartbeats struct {
|
||||
|
||||
lock sync.RWMutex
|
||||
coordinators map[uuid.UUID]time.Time
|
||||
timer *clock.Timer
|
||||
timer *quartz.Timer
|
||||
|
||||
wg sync.WaitGroup
|
||||
|
||||
// for testing
|
||||
clock clock.Clock
|
||||
clock quartz.Clock
|
||||
}
|
||||
|
||||
func newHeartbeats(
|
||||
@@ -1482,7 +1482,7 @@ func newHeartbeats(
|
||||
ps pubsub.Pubsub, store database.Store,
|
||||
self uuid.UUID, update chan<- hbUpdate,
|
||||
firstHeartbeat chan<- struct{},
|
||||
clk clock.Clock,
|
||||
clk quartz.Clock,
|
||||
) *heartbeats {
|
||||
h := &heartbeats{
|
||||
ctx: ctx,
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/clock"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -21,6 +19,7 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/quartz"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbmock"
|
||||
@@ -51,7 +50,7 @@ func TestHeartbeats_Cleanup(t *testing.T) {
|
||||
mStore.EXPECT().CleanTailnetLostPeers(gomock.Any()).Times(2).Return(nil)
|
||||
mStore.EXPECT().CleanTailnetTunnels(gomock.Any()).Times(2).Return(nil)
|
||||
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
trap := mClock.Trap().TickerFunc("heartbeats", "cleanupLoop")
|
||||
defer trap.Close()
|
||||
|
||||
@@ -79,7 +78,7 @@ func TestHeartbeats_recvBeat_resetSkew(t *testing.T) {
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
trap := mClock.Trap().Until("heartbeats", "resetExpiryTimerWithLock")
|
||||
defer trap.Close()
|
||||
|
||||
@@ -130,7 +129,7 @@ func TestHeartbeats_LostCoordinator_MarkLost(t *testing.T) {
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mStore := dbmock.NewMockStore(ctrl)
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
@@ -388,7 +387,7 @@ func TestPGCoordinatorUnhealthy(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mStore := dbmock.NewMockStore(ctrl)
|
||||
ps := pubsub.NewInMemory()
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
tfTrap := mClock.Trap().TickerFunc("heartbeats", "sendBeats")
|
||||
defer tfTrap.Close()
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/clock"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -33,6 +31,7 @@ import (
|
||||
"github.com/coder/coder/v2/tailnet/proto"
|
||||
agpltest "github.com/coder/coder/v2/tailnet/test"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -339,7 +338,7 @@ func TestPGCoordinatorSingle_MissedHeartbeats(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
afTrap := mClock.Trap().AfterFunc("heartbeats", "recvBeat")
|
||||
defer afTrap.Close()
|
||||
rstTrap := mClock.Trap().TimerReset("heartbeats", "resetExpiryTimerWithLock")
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
name = "coder-${osArch}";
|
||||
# Updated with ./scripts/update-flake.sh`.
|
||||
# This should be updated whenever go.mod changes!
|
||||
vendorHash = "sha256-5R2FelgM9NRYlse309NukEVh25pvusO2FXZx1VuWGoo=";
|
||||
vendorHash = "sha256-0pLwV4zpu+3LEwGxuGgcrr5iHP8bDNYuHOSCsyDsv/g=";
|
||||
proxyVendor = true;
|
||||
src = ./.;
|
||||
nativeBuildInputs = with pkgs; [ getopt openssl zstd ];
|
||||
|
||||
@@ -87,6 +87,7 @@ require (
|
||||
github.com/cli/safeexec v1.0.1
|
||||
github.com/coder/flog v1.1.0
|
||||
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0
|
||||
github.com/coder/quartz v0.1.0
|
||||
github.com/coder/retry v1.5.1
|
||||
github.com/coder/terraform-provider-coder v0.23.0
|
||||
github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0
|
||||
|
||||
@@ -207,6 +207,8 @@ github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136 h1:0RgB61LcNs
|
||||
github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136/go.mod h1:VkD1P761nykiq75dz+4iFqIQIZka189tx1BQLOp0Skc=
|
||||
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
|
||||
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
|
||||
github.com/coder/quartz v0.1.0 h1:cLL+0g5l7xTf6ordRnUMMiZtRE8Sq5LxpghS63vEXrQ=
|
||||
github.com/coder/quartz v0.1.0/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA=
|
||||
github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc=
|
||||
github.com/coder/retry v1.5.1/go.mod h1:blHMk9vs6LkoRT9ZHyuZo360cufXEhrxqvEzeMtRGoY=
|
||||
github.com/coder/serpent v0.7.0 h1:zGpD2GlF3lKIVkMjNGKbkip88qzd5r/TRcc30X/SrT0=
|
||||
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"tailscale.com/wgengine/wgcfg/nmcfg"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/clock"
|
||||
"github.com/coder/coder/v2/tailnet/proto"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
const lostTimeout = 15 * time.Minute
|
||||
@@ -70,7 +70,7 @@ type configMaps struct {
|
||||
blockEndpoints bool
|
||||
|
||||
// for testing
|
||||
clock clock.Clock
|
||||
clock quartz.Clock
|
||||
}
|
||||
|
||||
func newConfigMaps(logger slog.Logger, engine engineConfigurable, nodeID tailcfg.NodeID, nodeKey key.NodePrivate, discoKey key.DiscoPublic) *configMaps {
|
||||
@@ -116,7 +116,7 @@ func newConfigMaps(logger slog.Logger, engine engineConfigurable, nodeID tailcfg
|
||||
}},
|
||||
},
|
||||
peers: make(map[uuid.UUID]*peerLifecycle),
|
||||
clock: clock.NewReal(),
|
||||
clock: quartz.NewReal(),
|
||||
}
|
||||
go c.configLoop()
|
||||
return c
|
||||
@@ -657,9 +657,9 @@ type peerLifecycle struct {
|
||||
node *tailcfg.Node
|
||||
lost bool
|
||||
lastHandshake time.Time
|
||||
lostTimer *clock.Timer
|
||||
lostTimer *quartz.Timer
|
||||
readyForHandshake bool
|
||||
readyForHandshakeTimer *clock.Timer
|
||||
readyForHandshakeTimer *quartz.Timer
|
||||
}
|
||||
|
||||
func (l *peerLifecycle) resetLostTimer() {
|
||||
|
||||
@@ -21,9 +21,9 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/clock"
|
||||
"github.com/coder/coder/v2/tailnet/proto"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func TestConfigMaps_setAddresses_different(t *testing.T) {
|
||||
@@ -195,7 +195,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_neverConfigures(t *testing.
|
||||
discoKey := key.NewDisco()
|
||||
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
|
||||
defer uut.close()
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
uut.clock = mClock
|
||||
|
||||
p1ID := uuid.UUID{1}
|
||||
@@ -239,7 +239,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_outOfOrder(t *testing.T) {
|
||||
discoKey := key.NewDisco()
|
||||
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
|
||||
defer uut.close()
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
uut.clock = mClock
|
||||
|
||||
p1ID := uuid.UUID{1}
|
||||
@@ -310,7 +310,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake(t *testing.T) {
|
||||
discoKey := key.NewDisco()
|
||||
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
|
||||
defer uut.close()
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
uut.clock = mClock
|
||||
|
||||
p1ID := uuid.UUID{1}
|
||||
@@ -381,7 +381,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_timeout(t *testing.T) {
|
||||
discoKey := key.NewDisco()
|
||||
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
|
||||
defer uut.close()
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
uut.clock = mClock
|
||||
|
||||
p1ID := uuid.UUID{1}
|
||||
@@ -566,7 +566,7 @@ func TestConfigMaps_updatePeers_lost(t *testing.T) {
|
||||
discoKey := key.NewDisco()
|
||||
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
|
||||
defer uut.close()
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
start := mClock.Now()
|
||||
uut.clock = mClock
|
||||
|
||||
@@ -651,7 +651,7 @@ func TestConfigMaps_updatePeers_lost_and_found(t *testing.T) {
|
||||
discoKey := key.NewDisco()
|
||||
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
|
||||
defer uut.close()
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
start := mClock.Now()
|
||||
uut.clock = mClock
|
||||
|
||||
@@ -736,7 +736,7 @@ func TestConfigMaps_setAllPeersLost(t *testing.T) {
|
||||
discoKey := key.NewDisco()
|
||||
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
|
||||
defer uut.close()
|
||||
mClock := clock.NewMock(t)
|
||||
mClock := quartz.NewMock(t)
|
||||
start := mClock.Now()
|
||||
uut.clock = mClock
|
||||
|
||||
|
||||
+4
-4
@@ -17,8 +17,8 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/apiversion"
|
||||
"github.com/coder/coder/v2/clock"
|
||||
"github.com/coder/coder/v2/tailnet/proto"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
type streamIDContextKey struct{}
|
||||
@@ -240,7 +240,7 @@ func (c communicator) loopResp() {
|
||||
}
|
||||
|
||||
type NetworkTelemetryBatcher struct {
|
||||
clock clock.Clock
|
||||
clock quartz.Clock
|
||||
frequency time.Duration
|
||||
maxSize int
|
||||
batchFn func(batch []*proto.TelemetryEvent)
|
||||
@@ -248,11 +248,11 @@ type NetworkTelemetryBatcher struct {
|
||||
mu sync.Mutex
|
||||
closed chan struct{}
|
||||
done chan struct{}
|
||||
ticker *clock.Ticker
|
||||
ticker *quartz.Ticker
|
||||
pending []*proto.TelemetryEvent
|
||||
}
|
||||
|
||||
func NewNetworkTelemetryBatcher(clk clock.Clock, frequency time.Duration, maxSize int, batchFn func(batch []*proto.TelemetryEvent)) *NetworkTelemetryBatcher {
|
||||
func NewNetworkTelemetryBatcher(clk quartz.Clock, frequency time.Duration, maxSize int, batchFn func(batch []*proto.TelemetryEvent)) *NetworkTelemetryBatcher {
|
||||
b := &NetworkTelemetryBatcher{
|
||||
clock: clk,
|
||||
frequency: frequency,
|
||||
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/clock"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
"github.com/coder/coder/v2/tailnet/proto"
|
||||
"github.com/coder/coder/v2/tailnet/tailnettest"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func TestClientService_ServeClient_V2(t *testing.T) {
|
||||
@@ -182,7 +182,7 @@ func TestNetworkTelemetryBatcher(t *testing.T) {
|
||||
|
||||
var (
|
||||
events = make(chan []*proto.TelemetryEvent, 64)
|
||||
mClock = clock.NewMock(t)
|
||||
mClock = quartz.NewMock(t)
|
||||
b = tailnet.NewNetworkTelemetryBatcher(mClock, time.Millisecond, 3, func(batch []*proto.TelemetryEvent) {
|
||||
assert.LessOrEqual(t, len(batch), 3)
|
||||
events <- batch
|
||||
|
||||
Reference in New Issue
Block a user