Skip to content

feat(scaletest/dashboard): integrate chromedp #9927

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Oct 2, 2023
Merged
Prev Previous commit
Next Next commit
use prng, add --rand-seed argument
  • Loading branch information
johnstcn committed Sep 29, 2023
commit cf101511c8a67fac3281bc9d7355c515da944f87
12 changes: 12 additions & 0 deletions cli/exp_scaletest.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"strconv"
Expand Down Expand Up @@ -1049,6 +1050,7 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
minWait time.Duration
maxWait time.Duration
headless bool
randSeed int64

client = &codersdk.Client{}
tracingFlags = &scaletestTracingFlags{}
Expand Down Expand Up @@ -1106,6 +1108,8 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
}

for _, usr := range users {
//nolint:gosec // not used for cryptographic purposes
rndGen := rand.New(rand.NewSource(randSeed))
name := fmt.Sprintf("dashboard-%s", usr.Username)
userTokResp, err := client.CreateToken(ctx, usr.ID.String(), codersdk.CreateTokenRequest{
Lifetime: 30 * 24 * time.Hour,
Expand All @@ -1126,6 +1130,7 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
Logger: logger.Named(name),
Headless: headless,
ActionFunc: dashboard.ClickRandomElement,
RandIntn: rndGen.Intn,
}
//nolint:gocritic
logger.Info(ctx, "runner config", slog.F("min_wait", minWait), slog.F("max_wait", maxWait), slog.F("headless", headless), slog.F("trace", tracingEnabled))
Expand Down Expand Up @@ -1189,6 +1194,13 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
Description: "Controls headless mode. Setting to false is useful for debugging.",
Value: clibase.BoolOf(&headless),
},
{
Flag: "rand-seed",
Env: "CODER_SCALETEST_DASHBOARD_RAND_SEED",
Default: "0",
Description: "Seed for the random number generator.",
Value: clibase.Int64Of(&randSeed),
},
}

tracingFlags.attach(&cmd.Options)
Expand Down
1 change: 1 addition & 0 deletions cli/exp_scaletest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ func TestScaleTestDashboard(t *testing.T) {
"--timeout", "5s",
"--scaletest-prometheus-address", "127.0.0.1:0",
"--scaletest-prometheus-wait", "0s",
"--rand-seed", "1234567890",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
Expand Down
41 changes: 21 additions & 20 deletions scaletest/dashboard/chromedp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package dashboard

import (
"context"
"math/rand"
"net/url"
"os"
"time"
Expand Down Expand Up @@ -91,43 +90,45 @@ var defaultTargets = []Target{
// If no elements are found, an error is returned.
// If more than one element is found, one is chosen at random.
// The label of the clicked element is returned.
func ClickRandomElement(ctx context.Context) (Label, Action, error) {
func ClickRandomElement(ctx context.Context, randIntn func(int) int) (Label, Action, error) {
var xpath Selector
var found bool
var err error
matches := make(map[Label]Selector)
waitFor := make(map[Label]Selector)
matches := make([]Target, 0)
for _, tgt := range defaultTargets {
xpath, found, err = randMatch(ctx, tgt.ClickOn)
xpath, found, err = randMatch(ctx, tgt.ClickOn, randIntn)
if err != nil {
return "", nil, xerrors.Errorf("find matches for %q: %w", tgt.ClickOn, err)
}
if !found {
continue
}
matches[tgt.Label] = xpath
waitFor[tgt.Label] = tgt.WaitFor
matches = append(matches, Target{
Label: tgt.Label,
ClickOn: xpath,
WaitFor: tgt.WaitFor,
})
}

if len(matches) == 0 {
return "", nil, xerrors.Errorf("no matches found")
}
match := pick(matches, randIntn)
// rely on map iteration order being random
for lbl, tgt := range matches {
act := func(actx context.Context) error {
if err := clickAndWait(actx, tgt, waitFor[lbl]); err != nil {
return xerrors.Errorf("click %q: %w", tgt, err)
}
return nil
act := func(actx context.Context) error {
if err := clickAndWait(actx, match.ClickOn, match.WaitFor); err != nil {
return xerrors.Errorf("click %q: %w", match.ClickOn, err)
}
return lbl, act, nil
return nil
}

return "", nil, xerrors.Errorf("no matches found")
return match.Label, act, nil
}

// randMatch returns a random match for the given selector.
// The returned selector is the full XPath of the matched node.
// If no matches are found, an error is returned.
// If multiple matches are found, one is chosen at random.
func randMatch(ctx context.Context, s Selector) (Selector, bool, error) {
func randMatch(ctx context.Context, s Selector, randIntn func(int) int) (Selector, bool, error) {
var nodes []*cdp.Node
err := chromedp.Run(ctx, chromedp.Nodes(s, &nodes, chromedp.NodeVisible, chromedp.AtLeast(0)))
if err != nil {
Expand All @@ -136,7 +137,7 @@ func randMatch(ctx context.Context, s Selector) (Selector, bool, error) {
if len(nodes) == 0 {
return "", false, nil
}
n := pick(nodes)
n := pick(nodes, randIntn)
return Selector(n.FullXPath()), true, nil
}

Expand Down Expand Up @@ -210,11 +211,11 @@ func visitMainPage(ctx context.Context, u *url.URL) error {

// pick chooses a random element from a slice.
// If the slice is empty, it returns the zero value of the type.
func pick[T any](s []T) T {
func pick[T any](s []T, randIntn func(int) int) T {
if len(s) == 0 {
var zero T
return zero
}
// nolint:gosec
return s[rand.Intn(len(s))]
return s[randIntn(len(s))]
}
4 changes: 3 additions & 1 deletion scaletest/dashboard/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ type Config struct {
// Headless controls headless mode for chromedp.
Headless bool `json:"headless"`
// ActionFunc is a function that returns an action to run.
ActionFunc func(ctx context.Context) (Label, Action, error) `json:"-"`
ActionFunc func(ctx context.Context, randIntn func(int) int) (Label, Action, error) `json:"-"`
// RandIntn is a function that returns a random number between 0 and n-1.
RandIntn func(int) int `json:"-"`
}

func (c Config) Validate() error {
Expand Down
2 changes: 1 addition & 1 deletion scaletest/dashboard/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (r *Runner) Run(ctx context.Context, _ string, _ io.Writer) error {
return nil
case <-t.C:
t.Reset(r.randWait())
l, act, err := r.cfg.ActionFunc(cdpCtx)
l, act, err := r.cfg.ActionFunc(cdpCtx, r.cfg.RandIntn)
if err != nil {
r.cfg.Logger.Error(ctx, "calling ActionFunc", slog.Error(err))
continue
Expand Down
8 changes: 6 additions & 2 deletions scaletest/dashboard/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ func Test_Run(t *testing.T) {
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)

Expand All @@ -48,12 +51,13 @@ func Test_Run(t *testing.T) {
MaxWait: 500 * time.Millisecond,
Logger: log,
Headless: true,
ActionFunc: func(ctx context.Context) (dashboard.Label, dashboard.Action, error) {
if rand.Intn(2) == 0 { //nolint:gosec // just for testing
ActionFunc: func(_ context.Context, rnd func(int) int) (dashboard.Label, dashboard.Action, error) {
if rnd(2) == 0 {
return "fails", failAction, nil
}
return "succeeds", successAction, nil
},
RandIntn: rg.Intn,
}
r := dashboard.NewRunner(client, m, cfg)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
Expand Down