Skip to content

Commit

Permalink
cmd: test command UX (#3370)
Browse files Browse the repository at this point in the history
Multiple changes to improve UX of the test command:
1. Add `charon test all` that runs all 5 test commands
2. Shorten the ENRs and MEV relays URLs hashes when outputting results
3. Sort the targets of the tests (so that it's easier to follow consecutive re-ran tests)

Also do some refactoring of the functions.

category: feature
ticket: none
  • Loading branch information
KaloyanTanev committed Nov 19, 2024
1 parent 88ebdbd commit 683918a
Show file tree
Hide file tree
Showing 10 changed files with 958 additions and 888 deletions.
2 changes: 1 addition & 1 deletion cmd/ascii.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func validatorASCII() []string {

func mevASCII() []string {
return []string{
"__ __ ________ __ ",
" __ __ ________ __ ",
"| \\/ | ____\\ \\ / / ",
"| \\ / | |__ \\ \\ / / ",
"| |\\/| | __| \\ \\/ / ",
Expand Down
1 change: 1 addition & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func New() *cobra.Command {
newCombineCmd(newCombineFunc),
newAlphaCmd(
newTestCmd(
newTestAllCmd(runTestAll),
newTestPeersCmd(runTestPeers),
newTestBeaconCmd(runTestBeacon),
newTestValidatorCmd(runTestValidator),
Expand Down
88 changes: 85 additions & 3 deletions cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptrace"
"os"
"os/signal"
"slices"
"sort"
"strings"
"syscall"
Expand All @@ -16,6 +19,7 @@ import (

"github.com/pelletier/go-toml/v2"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/exp/maps"

"github.com/obolnetwork/charon/app/errors"
Expand All @@ -34,6 +38,7 @@ const (
validatorTestCategory = "validator"
mevTestCategory = "mev"
performanceTestCategory = "performance"
allTestCategory = "all"
)

type testConfig struct {
Expand Down Expand Up @@ -62,6 +67,13 @@ func bindTestFlags(cmd *cobra.Command, config *testConfig) {
cmd.Flags().BoolVar(&config.Quiet, "quiet", false, "Do not print test results to stdout.")
}

func bindTestLogFlags(flags *pflag.FlagSet, config *log.Config) {
flags.StringVar(&config.Format, "log-format", "console", "Log format; console, logfmt or json")
flags.StringVar(&config.Level, "log-level", "info", "Log level; debug, info, warn or error")
flags.StringVar(&config.Color, "log-color", "auto", "Log color; auto, force, disable.")
flags.StringVar(&config.LogOutputPath, "log-output-path", "", "Path in which to write on-disk logs.")
}

func listTestCases(cmd *cobra.Command) []string {
var testCaseNames []testCaseName
switch cmd.Name() {
Expand All @@ -76,6 +88,16 @@ func listTestCases(cmd *cobra.Command) []string {
testCaseNames = maps.Keys(supportedMEVTestCases())
case performanceTestCategory:
testCaseNames = maps.Keys(supportedPerformanceTestCases())
case allTestCategory:
testCaseNames = slices.Concat(
maps.Keys(supportedPeerTestCases()),
maps.Keys(supportedSelfTestCases()),
maps.Keys(supportedRelayTestCases()),
maps.Keys(supportedBeaconTestCases()),
maps.Keys(supportedValidatorTestCases()),
maps.Keys(supportedMEVTestCases()),
maps.Keys(supportedPerformanceTestCases()),
)
default:
log.Warn(cmd.Context(), "Unknown command for listing test cases", nil, z.Str("name", cmd.Name()))
}
Expand Down Expand Up @@ -229,12 +251,14 @@ func writeResultToWriter(res testCategoryResult, w io.Writer) error {
lines = append(lines, "")
lines = append(lines, fmt.Sprintf("%-64s%s", "TEST NAME", "RESULT"))
suggestions := []string{}
for target, testResults := range res.Targets {
if target != "" && len(testResults) > 0 {
targets := maps.Keys(res.Targets)
slices.Sort(targets)
for _, target := range targets {
if target != "" && len(res.Targets[target]) > 0 {
lines = append(lines, "")
lines = append(lines, target)
}
for _, singleTestRes := range testResults {
for _, singleTestRes := range res.Targets[target] {
testOutput := ""
testOutput += fmt.Sprintf("%-64s", singleTestRes.Name)
if singleTestRes.Measurement != "" {
Expand Down Expand Up @@ -273,6 +297,30 @@ func writeResultToWriter(res testCategoryResult, w io.Writer) error {
return nil
}

func evaluateHighestRTTScores(testResCh chan time.Duration, testRes testResult, avg time.Duration, poor time.Duration) testResult {
highestRTT := time.Duration(0)
for rtt := range testResCh {
if rtt > highestRTT {
highestRTT = rtt
}
}

return evaluateRTT(highestRTT, testRes, avg, poor)
}

func evaluateRTT(rtt time.Duration, testRes testResult, avg time.Duration, poor time.Duration) testResult {
if rtt == 0 || rtt > poor {
testRes.Verdict = testVerdictPoor
} else if rtt > avg {
testRes.Verdict = testVerdictAvg
} else {
testRes.Verdict = testVerdictGood
}
testRes.Measurement = Duration{rtt}.String()

return testRes
}

func calculateScore(results []testResult) categoryScore {
// TODO(kalo): calculate score more elaborately (potentially use weights)
avg := 0
Expand Down Expand Up @@ -348,3 +396,37 @@ func sleepWithContext(ctx context.Context, d time.Duration) {
case <-timer.C:
}
}

func requestRTT(ctx context.Context, url string, method string, body io.Reader, expectedStatus int) (time.Duration, error) {
var start time.Time
var firstByte time.Duration

trace := &httptrace.ClientTrace{
GotFirstResponseByte: func() {
firstByte = time.Since(start)
},
}

start = time.Now()
req, err := http.NewRequestWithContext(httptrace.WithClientTrace(ctx, trace), method, url, body)
if err != nil {
return 0, errors.Wrap(err, "create new request with trace and context")
}

resp, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()

if resp.StatusCode != expectedStatus {
data, err := io.ReadAll(resp.Body)
if err != nil {
log.Warn(ctx, "Unexpected status code", nil, z.Int("status_code", resp.StatusCode), z.Int("expected_status_code", expectedStatus), z.Str("endpoint", url))
} else {
log.Warn(ctx, "Unexpected status code", nil, z.Int("status_code", resp.StatusCode), z.Int("expected_status_code", expectedStatus), z.Str("endpoint", url), z.Str("body", string(data)))
}
}

return firstByte, nil
}
97 changes: 97 additions & 0 deletions cmd/testall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1

package cmd

import (
"context"
"io"

"github.com/spf13/cobra"

"github.com/obolnetwork/charon/app/errors"
)

type testAllConfig struct {
testConfig
Peers testPeersConfig
Beacon testBeaconConfig
Validator testValidatorConfig
MEV testMEVConfig
Performance testPerformanceConfig
}

func newTestAllCmd(runFunc func(context.Context, io.Writer, testAllConfig) error) *cobra.Command {
var config testAllConfig

cmd := &cobra.Command{
Use: "all",
Short: "Run tests towards peer nodes, beacon nodes, validator client, MEV relays, own hardware and internet connectivity.",
Long: `Run tests towards peer nodes, beacon nodes, validator client, MEV relays, own hardware and internet connectivity. Verify that Charon can efficiently do its duties on the tested setup.`,
Args: cobra.NoArgs,
PreRunE: func(cmd *cobra.Command, _ []string) error {
return mustOutputToFileOnQuiet(cmd)
},
RunE: func(cmd *cobra.Command, _ []string) error {
return runFunc(cmd.Context(), cmd.OutOrStdout(), config)
},
}

bindTestFlags(cmd, &config.testConfig)

bindTestPeersFlags(cmd, &config.Peers, "peers-")
bindTestBeaconFlags(cmd, &config.Beacon, "beacon-")
bindTestValidatorFlags(cmd, &config.Validator, "validator-")
bindTestMEVFlags(cmd, &config.MEV, "mev-")
bindTestPerformanceFlags(cmd, &config.Performance, "performance-")

bindP2PFlags(cmd, &config.Peers.P2P)
bindDataDirFlag(cmd.Flags(), &config.Peers.DataDir)
bindTestLogFlags(cmd.Flags(), &config.Peers.Log)

wrapPreRunE(cmd, func(cmd *cobra.Command, _ []string) error {
testCasesPresent := cmd.Flags().Lookup("test-cases").Changed

if testCasesPresent {
//nolint:revive // we use our own version of the errors package
return errors.New("test-cases cannot be specified when explicitly running all test cases.")
}

return nil
})

return cmd
}

func runTestAll(ctx context.Context, w io.Writer, cfg testAllConfig) (err error) {
cfg.Beacon.testConfig = cfg.testConfig
err = runTestBeacon(ctx, w, cfg.Beacon)
if err != nil {
return err
}

cfg.Validator.testConfig = cfg.testConfig
err = runTestValidator(ctx, w, cfg.Validator)
if err != nil {
return err
}

cfg.MEV.testConfig = cfg.testConfig
err = runTestMEV(ctx, w, cfg.MEV)
if err != nil {
return err
}

cfg.Performance.testConfig = cfg.testConfig
err = runTestPerformance(ctx, w, cfg.Performance)
if err != nil {
return err
}

cfg.Peers.testConfig = cfg.testConfig
err = runTestPeers(ctx, w, cfg.Peers)
if err != nil {
return err
}

return nil
}
Loading

0 comments on commit 683918a

Please sign in to comment.