package dashboard import ( "context" "errors" "io" "math/rand" "time" "golang.org/x/xerrors" "cdr.dev/slog" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/scaletest/harness" ) type Runner struct { client *codersdk.Client cfg Config metrics Metrics } var ( _ harness.Runnable = &Runner{} _ harness.Cleanable = &Runner{} ) func NewRunner(client *codersdk.Client, metrics Metrics, cfg Config) *Runner { client.Trace = cfg.Trace if cfg.WaitLoaded == nil { cfg.WaitLoaded = waitForWorkspacesPageLoaded } if cfg.ActionFunc == nil { cfg.ActionFunc = clickRandomElement } if cfg.Screenshot == nil { cfg.Screenshot = Screenshot } if cfg.RandIntn == nil { cfg.RandIntn = rand.Intn } if cfg.InitChromeDPCtx == nil { cfg.InitChromeDPCtx = initChromeDPCtx } return &Runner{ client: client, cfg: cfg, metrics: metrics, } } func (r *Runner) Run(ctx context.Context, _ string, _ io.Writer) error { err := r.runUntilDeadlineExceeded(ctx) // If the context deadline exceeded, don't return an error. // This just means the test finished. if err == nil || errors.Is(err, context.DeadlineExceeded) { return nil } return err } func (r *Runner) runUntilDeadlineExceeded(ctx context.Context) error { if r.client == nil { return xerrors.Errorf("client is nil") } me, err := r.client.User(ctx, codersdk.Me) if err != nil { return xerrors.Errorf("get scaletest user: %w", err) } //nolint:gocritic r.cfg.Logger.Info(ctx, "running as user", slog.F("username", me.Username)) if len(me.OrganizationIDs) == 0 { return xerrors.Errorf("user has no organizations") } cdpCtx, cdpCancel, err := r.cfg.InitChromeDPCtx(ctx, r.cfg.Logger, r.client.URL, r.client.SessionToken(), r.cfg.Headless) if err != nil { return xerrors.Errorf("init chromedp ctx: %w", err) } defer cdpCancel() t := time.NewTicker(1) // First one should be immediate defer t.Stop() r.cfg.Logger.Info(ctx, "waiting for workspaces page to load") loadWorkspacePageDeadline := time.Now().Add(r.cfg.Interval) if err := r.cfg.WaitLoaded(cdpCtx, loadWorkspacePageDeadline); err != nil { return xerrors.Errorf("wait for workspaces page to load: %w", err) } for { select { case <-cdpCtx.Done(): return nil case <-t.C: var offset time.Duration if r.cfg.Jitter > 0 { offset = time.Duration(r.cfg.RandIntn(int(2*r.cfg.Jitter)) - int(r.cfg.Jitter)) } wait := r.cfg.Interval + offset actionCompleteByDeadline := time.Now().Add(wait) t.Reset(wait) l, act, err := r.cfg.ActionFunc(cdpCtx, r.cfg.Logger, r.cfg.RandIntn, actionCompleteByDeadline) if err != nil { r.cfg.Logger.Error(ctx, "calling ActionFunc", slog.Error(err)) sPath, sErr := r.cfg.Screenshot(cdpCtx, me.Username) if sErr != nil { r.cfg.Logger.Error(ctx, "screenshot failed", slog.Error(sErr)) } r.cfg.Logger.Info(ctx, "screenshot saved", slog.F("path", sPath)) continue } start := time.Now() err = act(cdpCtx) elapsed := time.Since(start) r.metrics.ObserveDuration(string(l), elapsed) if err != nil { r.metrics.IncErrors(string(l)) //nolint:gocritic r.cfg.Logger.Error(ctx, "action failed", slog.F("label", l), slog.Error(err)) sPath, sErr := r.cfg.Screenshot(cdpCtx, me.Username+"-"+string(l)) if sErr != nil { r.cfg.Logger.Error(ctx, "screenshot failed", slog.Error(sErr)) } r.cfg.Logger.Info(ctx, "screenshot saved", slog.F("path", sPath)) } else { //nolint:gocritic r.cfg.Logger.Info(ctx, "action success", slog.F("label", l)) } } } } func (*Runner) Cleanup(_ context.Context, _ string, _ io.Writer) error { return nil }