mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
141 lines
3.5 KiB
Go
141 lines
3.5 KiB
Go
package dashboard_test
|
|
|
|
import (
|
|
"context"
|
|
"math/rand"
|
|
"net/url"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog"
|
|
"cdr.dev/slog/sloggers/slogtest"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/scaletest/dashboard"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func Test_Run(t *testing.T) {
|
|
t.Parallel()
|
|
if testutil.RaceEnabled() {
|
|
t.Skip("skipping timing-sensitive test because of race detector")
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("skipping test on Windows")
|
|
}
|
|
|
|
successAction := func(_ context.Context) error {
|
|
<-time.After(testutil.IntervalFast)
|
|
return nil
|
|
}
|
|
|
|
failAction := func(_ context.Context) error {
|
|
<-time.After(testutil.IntervalMedium)
|
|
return assert.AnError
|
|
}
|
|
|
|
//nolint: gosec // just for testing
|
|
rg := rand.New(rand.NewSource(0)) // deterministic for testing
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
log := slogtest.Make(t, &slogtest.Options{
|
|
IgnoreErrors: true,
|
|
})
|
|
m := &testMetrics{}
|
|
var (
|
|
waitLoadedCalled atomic.Bool
|
|
screenshotCalled atomic.Bool
|
|
)
|
|
cancelDone := make(chan struct{})
|
|
cfg := dashboard.Config{
|
|
Interval: 500 * time.Millisecond,
|
|
Jitter: 100 * time.Millisecond,
|
|
Logger: log,
|
|
Headless: true,
|
|
WaitLoaded: func(_ context.Context, _ time.Time) error {
|
|
waitLoadedCalled.Store(true)
|
|
return nil
|
|
},
|
|
ActionFunc: func(_ context.Context, _ slog.Logger, rnd func(int) int, _ time.Time) (dashboard.Label, dashboard.Action, error) {
|
|
if rnd(2) == 0 {
|
|
return "fails", failAction, nil
|
|
}
|
|
return "succeeds", successAction, nil
|
|
},
|
|
Screenshot: func(_ context.Context, name string) (string, error) {
|
|
screenshotCalled.Store(true)
|
|
return "/fake/path/to/" + name + ".png", nil
|
|
},
|
|
RandIntn: rg.Intn,
|
|
InitChromeDPCtx: func(ctx context.Context, _ slog.Logger, _ *url.URL, _ string, _ bool) (context.Context, context.CancelFunc, error) {
|
|
return ctx, func() { close(cancelDone) }, nil
|
|
},
|
|
}
|
|
r := dashboard.NewRunner(client, m, cfg)
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
|
t.Cleanup(cancel)
|
|
done := make(chan error)
|
|
go func() {
|
|
defer close(done)
|
|
done <- r.Run(ctx, "", nil)
|
|
}()
|
|
err, ok := <-done
|
|
assert.True(t, ok)
|
|
require.NoError(t, err)
|
|
_, ok = <-cancelDone
|
|
require.False(t, ok, "cancel should have been called")
|
|
|
|
for _, dur := range m.ObservedDurations["succeeds"] {
|
|
assert.NotZero(t, dur)
|
|
}
|
|
for _, dur := range m.ObservedDurations["fails"] {
|
|
assert.NotZero(t, dur)
|
|
}
|
|
assert.Zero(t, m.Errors["succeeds"])
|
|
assert.NotZero(t, m.Errors["fails"])
|
|
}
|
|
|
|
type testMetrics struct {
|
|
sync.RWMutex
|
|
ObservedDurations map[string][]float64
|
|
Errors map[string]int
|
|
Statuses map[string]map[string]int
|
|
}
|
|
|
|
func (m *testMetrics) ObserveDuration(action string, d time.Duration) {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
if m.ObservedDurations == nil {
|
|
m.ObservedDurations = make(map[string][]float64)
|
|
}
|
|
m.ObservedDurations[action] = append(m.ObservedDurations[action], d.Seconds())
|
|
}
|
|
|
|
func (m *testMetrics) IncErrors(action string) {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
if m.Errors == nil {
|
|
m.Errors = make(map[string]int)
|
|
}
|
|
m.Errors[action]++
|
|
}
|
|
|
|
func (m *testMetrics) IncStatuses(action string, code string) {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
if m.Statuses == nil {
|
|
m.Statuses = make(map[string]map[string]int)
|
|
}
|
|
if m.Statuses[action] == nil {
|
|
m.Statuses[action] = make(map[string]int)
|
|
}
|
|
m.Statuses[action][code]++
|
|
}
|