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) } }