You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

365 lines
9.1 KiB

package main
import (
"context"
"fmt"
stdlog "log"
"math/rand"
"os"
"strconv"
"time"
"github.com/spf13/cobra"
"github.com/tendermint/tendermint/libs/log"
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
)
const randomSeed = 2308084734268
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
logger, err := log.NewDefaultLogger(log.LogFormatPlain, log.LogLevelInfo)
if err != nil {
stdlog.Fatal(err)
}
NewCLI(logger).Run(ctx, logger)
}
// CLI is the Cobra-based command-line interface.
type CLI struct {
root *cobra.Command
testnet *e2e.Testnet
preserve bool
}
// NewCLI sets up the CLI.
func NewCLI(logger log.Logger) *CLI {
cli := &CLI{}
cli.root = &cobra.Command{
Use: "runner",
Short: "End-to-end test runner",
SilenceUsage: true,
SilenceErrors: true, // we'll output them ourselves in Run()
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
file, err := cmd.Flags().GetString("file")
if err != nil {
return err
}
testnet, err := e2e.LoadTestnet(file)
if err != nil {
return err
}
cli.testnet = testnet
return nil
},
RunE: func(cmd *cobra.Command, args []string) (err error) {
if err = Cleanup(logger, cli.testnet); err != nil {
return err
}
defer func() {
if cli.preserve {
logger.Info("Preserving testnet contents because -preserve=true")
} else if err != nil {
logger.Info("Preserving testnet that encountered error",
"err", err)
} else if err := Cleanup(logger, cli.testnet); err != nil {
logger.Error("error cleaning up testnet contents", "err", err)
}
}()
if err = Setup(logger, cli.testnet); err != nil {
return err
}
r := rand.New(rand.NewSource(randomSeed)) // nolint: gosec
chLoadResult := make(chan error)
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
lctx, loadCancel := context.WithCancel(ctx)
defer loadCancel()
go func() {
chLoadResult <- Load(lctx, logger, r, cli.testnet)
}()
startAt := time.Now()
if err = Start(ctx, logger, cli.testnet); err != nil {
return err
}
if err = Wait(ctx, logger, cli.testnet, 5); err != nil { // allow some txs to go through
return err
}
if cli.testnet.HasPerturbations() {
if err = Perturb(ctx, logger, cli.testnet); err != nil {
return err
}
if err = Wait(ctx, logger, cli.testnet, 5); err != nil { // allow some txs to go through
return err
}
}
if cli.testnet.Evidence > 0 {
if err = InjectEvidence(ctx, logger, r, cli.testnet, cli.testnet.Evidence); err != nil {
return err
}
if err = Wait(ctx, logger, cli.testnet, 5); err != nil { // ensure chain progress
return err
}
}
// to help make sure that we don't run into
// situations where 0 transactions have
// happened on quick cases, we make sure that
// it's been at least 10s before canceling the
// load generator.
//
// TODO allow the load generator to report
// successful transactions to avoid needing
// this sleep.
if rest := time.Since(startAt); rest < 15*time.Second {
time.Sleep(15*time.Second - rest)
}
loadCancel()
if err = <-chLoadResult; err != nil {
return fmt.Errorf("transaction load failed: %w", err)
}
if err = Wait(ctx, logger, cli.testnet, 5); err != nil { // wait for network to settle before tests
return err
}
if err := Test(logger, cli.testnet); err != nil {
return err
}
return nil
},
}
cli.root.PersistentFlags().StringP("file", "f", "", "Testnet TOML manifest")
_ = cli.root.MarkPersistentFlagRequired("file")
cli.root.Flags().BoolVarP(&cli.preserve, "preserve", "p", false,
"Preserves the running of the test net after tests are completed")
cli.root.SetHelpCommand(&cobra.Command{
Use: "no-help",
Hidden: true,
})
cli.root.AddCommand(&cobra.Command{
Use: "setup",
Short: "Generates the testnet directory and configuration",
RunE: func(cmd *cobra.Command, args []string) error {
return Setup(logger, cli.testnet)
},
})
cli.root.AddCommand(&cobra.Command{
Use: "start",
Short: "Starts the Docker testnet, waiting for nodes to become available",
RunE: func(cmd *cobra.Command, args []string) error {
_, err := os.Stat(cli.testnet.Dir)
if os.IsNotExist(err) {
err = Setup(logger, cli.testnet)
}
if err != nil {
return err
}
return Start(cmd.Context(), logger, cli.testnet)
},
})
cli.root.AddCommand(&cobra.Command{
Use: "perturb",
Short: "Perturbs the Docker testnet, e.g. by restarting or disconnecting nodes",
RunE: func(cmd *cobra.Command, args []string) error {
return Perturb(cmd.Context(), logger, cli.testnet)
},
})
cli.root.AddCommand(&cobra.Command{
Use: "wait",
Short: "Waits for a few blocks to be produced and all nodes to catch up",
RunE: func(cmd *cobra.Command, args []string) error {
return Wait(cmd.Context(), logger, cli.testnet, 5)
},
})
cli.root.AddCommand(&cobra.Command{
Use: "stop",
Short: "Stops the Docker testnet",
RunE: func(cmd *cobra.Command, args []string) error {
logger.Info("Stopping testnet")
return execCompose(cli.testnet.Dir, "down")
},
})
cli.root.AddCommand(&cobra.Command{
Use: "pause",
Short: "Pauses the Docker testnet",
RunE: func(cmd *cobra.Command, args []string) error {
logger.Info("Pausing testnet")
return execCompose(cli.testnet.Dir, "pause")
},
})
cli.root.AddCommand(&cobra.Command{
Use: "resume",
Short: "Resumes the Docker testnet",
RunE: func(cmd *cobra.Command, args []string) error {
logger.Info("Resuming testnet")
return execCompose(cli.testnet.Dir, "unpause")
},
})
cli.root.AddCommand(&cobra.Command{
Use: "load",
Short: "Generates transaction load until the command is canceled",
RunE: func(cmd *cobra.Command, args []string) (err error) {
return Load(
cmd.Context(),
logger,
rand.New(rand.NewSource(randomSeed)), // nolint: gosec
cli.testnet,
)
},
})
cli.root.AddCommand(&cobra.Command{
Use: "evidence [amount]",
Args: cobra.MaximumNArgs(1),
Short: "Generates and broadcasts evidence to a random node",
RunE: func(cmd *cobra.Command, args []string) (err error) {
amount := 1
if len(args) == 1 {
amount, err = strconv.Atoi(args[0])
if err != nil {
return err
}
}
return InjectEvidence(
cmd.Context(),
logger,
rand.New(rand.NewSource(randomSeed)), // nolint: gosec
cli.testnet,
amount,
)
},
})
cli.root.AddCommand(&cobra.Command{
Use: "test",
Short: "Runs test cases against a running testnet",
RunE: func(cmd *cobra.Command, args []string) error {
return Test(logger, cli.testnet)
},
})
cli.root.AddCommand(&cobra.Command{
Use: "cleanup",
Short: "Removes the testnet directory",
RunE: func(cmd *cobra.Command, args []string) error {
return Cleanup(logger, cli.testnet)
},
})
cli.root.AddCommand(&cobra.Command{
Use: "logs [node]",
Short: "Shows the testnet or a specefic node's logs",
Example: "runner logs validator03",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return execComposeVerbose(cli.testnet.Dir, append([]string{"logs", "--no-color"}, args...)...)
},
})
cli.root.AddCommand(&cobra.Command{
Use: "tail [node]",
Short: "Tails the testnet or a specific node's logs",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 1 {
return execComposeVerbose(cli.testnet.Dir, "logs", "--follow", args[0])
}
return execComposeVerbose(cli.testnet.Dir, "logs", "--follow")
},
})
cli.root.AddCommand(&cobra.Command{
Use: "benchmark",
Short: "Benchmarks testnet",
Long: `Benchmarks the following metrics:
Mean Block Interval
Standard Deviation
Min Block Interval
Max Block Interval
over a 100 block sampling period.
Does not run any perbutations.
`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := Cleanup(logger, cli.testnet); err != nil {
return err
}
defer func() {
if err := Cleanup(logger, cli.testnet); err != nil {
logger.Error("error cleaning up testnet contents", "err", err)
}
}()
if err := Setup(logger, cli.testnet); err != nil {
return err
}
chLoadResult := make(chan error)
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
r := rand.New(rand.NewSource(randomSeed)) // nolint: gosec
lctx, loadCancel := context.WithCancel(ctx)
defer loadCancel()
go func() {
chLoadResult <- Load(lctx, logger, r, cli.testnet)
}()
if err := Start(ctx, logger, cli.testnet); err != nil {
return err
}
if err := Wait(ctx, logger, cli.testnet, 5); err != nil { // allow some txs to go through
return err
}
// we benchmark performance over the next 100 blocks
if err := Benchmark(ctx, logger, cli.testnet, 100); err != nil {
return err
}
loadCancel()
if err := <-chLoadResult; err != nil {
return err
}
return nil
},
})
return cli
}
// Run runs the CLI.
func (cli *CLI) Run(ctx context.Context, logger log.Logger) {
if err := cli.root.ExecuteContext(ctx); err != nil {
logger.Error(err.Error())
os.Exit(1)
}
}