package main import ( "context" "fmt" "os" "github.com/spf13/cobra" "github.com/tendermint/tendermint/libs/log" e2e "github.com/tendermint/tendermint/test/e2e/pkg" ) var ( logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout)) ) 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, loadCancel := context.WithCancel(context.Background()) defer loadCancel() go func() { err := Load(ctx, cli.testnet) if err != nil { logger.Error(fmt.Sprintf("Transaction load failed: %v", err.Error())) } chLoadResult <- err }() if err := Start(cli.testnet); err != nil { return err } if lastMisbehavior := cli.testnet.LastMisbehaviorHeight(); lastMisbehavior > 0 { // wait for misbehaviors before starting perturbations. We do a separate // wait for another 5 blocks, since the last misbehavior height may be // in the past depending on network startup ordering. if err := WaitUntil(cli.testnet, lastMisbehavior); err != nil { return err } } if err := Wait(cli.testnet, 5); err != nil { // allow some txs to go through return err } if cli.testnet.HasPerturbations() { if err := Perturb(cli.testnet); err != nil { return err } if err := Wait(cli.testnet, 5); err != nil { // allow some txs to go through return err } } loadCancel() if err := <-chLoadResult; err != nil { return err } if err := Wait(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(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(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(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: "load", Short: "Generates transaction load until the command is cancelled", RunE: func(cmd *cobra.Command, args []string) error { return Load(context.Background(), cli.testnet) }, }) 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 valiator03", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 1 { return execComposeVerbose(cli.testnet.Dir, "logs", args[0]) } return execComposeVerbose(cli.testnet.Dir, "logs") }, }) 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") }, }) return cli } // Run runs the CLI. func (cli *CLI) Run() { if err := cli.root.Execute(); err != nil { logger.Error(err.Error()) os.Exit(1) } }