package main import ( "context" "fmt" "os" "strconv" "github.com/spf13/cobra" "github.com/tendermint/tendermint/libs/log" e2e "github.com/tendermint/tendermint/test/e2e/pkg" ) var ( logger = log.MustNewDefaultLogger(log.LogFormatPlain, log.LogLevelInfo, false) ) func main() { NewCLI().Run() } // 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() *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) error { if err := Cleanup(cli.testnet); err != nil { return err } if err := Setup(cli.testnet); err != nil { return err } chLoadResult := make(chan error) ctx, cancel := context.WithCancel(cmd.Context()) defer cancel() lctx, loadCancel := context.WithCancel(ctx) defer loadCancel() go func() { chLoadResult <- Load(lctx, cli.testnet) }() if err := Start(ctx, cli.testnet); err != nil { return err } if err := Wait(ctx, cli.testnet, 5); err != nil { // allow some txs to go through return err } if cli.testnet.HasPerturbations() { if err := Perturb(ctx, cli.testnet); err != nil { return err } if err := Wait(ctx, cli.testnet, 5); err != nil { // allow some txs to go through return err } } if cli.testnet.Evidence > 0 { if err := InjectEvidence(ctx, cli.testnet, cli.testnet.Evidence); err != nil { return err } if err := Wait(ctx, cli.testnet, 5); err != nil { // ensure chain progress return err } } loadCancel() if err := <-chLoadResult; err != nil { return fmt.Errorf("transaction load failed: %w", err) } if err := Wait(ctx, cli.testnet, 5); err != nil { // wait for network to settle before tests return err } if err := Test(cli.testnet); err != nil { return err } if !cli.preserve { if err := Cleanup(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(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(cli.testnet) } if err != nil { return err } return Start(cmd.Context(), 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(), 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(), 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(context.Background(), 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(), 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(cli.testnet) }, }) cli.root.AddCommand(&cobra.Command{ Use: "cleanup", Short: "Removes the testnet directory", RunE: func(cmd *cobra.Command, args []string) error { return Cleanup(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(cli.testnet); err != nil { return err } if err := Setup(cli.testnet); err != nil { return err } chLoadResult := make(chan error) ctx, cancel := context.WithCancel(cmd.Context()) defer cancel() lctx, loadCancel := context.WithCancel(ctx) defer loadCancel() go func() { err := Load(lctx, cli.testnet) chLoadResult <- err }() if err := Start(ctx, cli.testnet); err != nil { return err } if err := Wait(ctx, 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, cli.testnet, 100); err != nil { return err } loadCancel() if err := <-chLoadResult; err != nil { return err } if err := Cleanup(cli.testnet); err != nil { return err } return nil }, }) return cli } // Run runs the CLI. func (cli *CLI) Run() { if err := cli.root.Execute(); err != nil { logger.Error(err.Error()) os.Exit(1) } }