github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/test/e2e/runner/main.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math/rand"
     8  	"os"
     9  	"strconv"
    10  
    11  	"github.com/spf13/cobra"
    12  
    13  	"github.com/badrootd/nibiru-cometbft/libs/log"
    14  	e2e "github.com/badrootd/nibiru-cometbft/test/e2e/pkg"
    15  	"github.com/badrootd/nibiru-cometbft/test/e2e/pkg/infra"
    16  	"github.com/badrootd/nibiru-cometbft/test/e2e/pkg/infra/docker"
    17  )
    18  
    19  const randomSeed = 2308084734268
    20  
    21  var logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout))
    22  
    23  func main() {
    24  	NewCLI().Run()
    25  }
    26  
    27  // CLI is the Cobra-based command-line interface.
    28  type CLI struct {
    29  	root     *cobra.Command
    30  	testnet  *e2e.Testnet
    31  	preserve bool
    32  	infp     infra.Provider
    33  }
    34  
    35  // NewCLI sets up the CLI.
    36  func NewCLI() *CLI {
    37  	cli := &CLI{}
    38  	cli.root = &cobra.Command{
    39  		Use:           "runner",
    40  		Short:         "End-to-end test runner",
    41  		SilenceUsage:  true,
    42  		SilenceErrors: true, // we'll output them ourselves in Run()
    43  		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
    44  			file, err := cmd.Flags().GetString("file")
    45  			if err != nil {
    46  				return err
    47  			}
    48  			m, err := e2e.LoadManifest(file)
    49  			if err != nil {
    50  				return err
    51  			}
    52  
    53  			inft, err := cmd.Flags().GetString("infrastructure-type")
    54  			if err != nil {
    55  				return err
    56  			}
    57  
    58  			var ifd e2e.InfrastructureData
    59  			switch inft {
    60  			case "docker":
    61  				var err error
    62  				ifd, err = e2e.NewDockerInfrastructureData(m)
    63  				if err != nil {
    64  					return err
    65  				}
    66  			case "digital-ocean":
    67  				p, err := cmd.Flags().GetString("infrastructure-data")
    68  				if err != nil {
    69  					return err
    70  				}
    71  				if p == "" {
    72  					return errors.New("'--infrastructure-data' must be set when using the 'digital-ocean' infrastructure-type")
    73  				}
    74  				ifd, err = e2e.InfrastructureDataFromFile(p)
    75  				if err != nil {
    76  					return fmt.Errorf("parsing infrastructure data: %s", err)
    77  				}
    78  			default:
    79  				return fmt.Errorf("unknown infrastructure type '%s'", inft)
    80  			}
    81  
    82  			testnet, err := e2e.LoadTestnet(m, file, ifd)
    83  			if err != nil {
    84  				return fmt.Errorf("loading testnet: %s", err)
    85  			}
    86  
    87  			cli.testnet = testnet
    88  			cli.infp = &infra.NoopProvider{}
    89  			if inft == "docker" {
    90  				cli.infp = &docker.Provider{Testnet: testnet}
    91  			}
    92  			return nil
    93  		},
    94  		RunE: func(cmd *cobra.Command, args []string) error {
    95  			if err := Cleanup(cli.testnet); err != nil {
    96  				return err
    97  			}
    98  			if err := Setup(cli.testnet, cli.infp); err != nil {
    99  				return err
   100  			}
   101  
   102  			r := rand.New(rand.NewSource(randomSeed)) //nolint: gosec
   103  
   104  			chLoadResult := make(chan error)
   105  			ctx, loadCancel := context.WithCancel(context.Background())
   106  			defer loadCancel()
   107  			go func() {
   108  				err := Load(ctx, cli.testnet)
   109  				if err != nil {
   110  					logger.Error(fmt.Sprintf("Transaction load failed: %v", err.Error()))
   111  				}
   112  				chLoadResult <- err
   113  			}()
   114  
   115  			if err := Start(cli.testnet); err != nil {
   116  				return err
   117  			}
   118  
   119  			if err := Wait(cli.testnet, 5); err != nil { // allow some txs to go through
   120  				return err
   121  			}
   122  
   123  			if cli.testnet.HasPerturbations() {
   124  				if err := Perturb(cli.testnet); err != nil {
   125  					return err
   126  				}
   127  				if err := Wait(cli.testnet, 5); err != nil { // allow some txs to go through
   128  					return err
   129  				}
   130  			}
   131  
   132  			if cli.testnet.Evidence > 0 {
   133  				if err := InjectEvidence(ctx, r, cli.testnet, cli.testnet.Evidence); err != nil {
   134  					return err
   135  				}
   136  				if err := Wait(cli.testnet, 5); err != nil { // ensure chain progress
   137  					return err
   138  				}
   139  			}
   140  
   141  			loadCancel()
   142  			if err := <-chLoadResult; err != nil {
   143  				return err
   144  			}
   145  			if err := Wait(cli.testnet, 5); err != nil { // wait for network to settle before tests
   146  				return err
   147  			}
   148  			if err := Test(cli.testnet); err != nil {
   149  				return err
   150  			}
   151  			if !cli.preserve {
   152  				if err := Cleanup(cli.testnet); err != nil {
   153  					return err
   154  				}
   155  			}
   156  			return nil
   157  		},
   158  	}
   159  
   160  	cli.root.PersistentFlags().StringP("file", "f", "", "Testnet TOML manifest")
   161  	_ = cli.root.MarkPersistentFlagRequired("file")
   162  
   163  	cli.root.PersistentFlags().StringP("infrastructure-type", "", "docker", "Backing infrastructure used to run the testnet. Either 'digital-ocean' or 'docker'")
   164  
   165  	cli.root.PersistentFlags().StringP("infrastructure-data", "", "", "path to the json file containing the infrastructure data. Only used if the 'infrastructure-type' is set to a value other than 'docker'")
   166  
   167  	cli.root.Flags().BoolVarP(&cli.preserve, "preserve", "p", false,
   168  		"Preserves the running of the test net after tests are completed")
   169  
   170  	cli.root.AddCommand(&cobra.Command{
   171  		Use:   "setup",
   172  		Short: "Generates the testnet directory and configuration",
   173  		RunE: func(cmd *cobra.Command, args []string) error {
   174  			return Setup(cli.testnet, cli.infp)
   175  		},
   176  	})
   177  
   178  	cli.root.AddCommand(&cobra.Command{
   179  		Use:   "start",
   180  		Short: "Starts the Docker testnet, waiting for nodes to become available",
   181  		RunE: func(cmd *cobra.Command, args []string) error {
   182  			_, err := os.Stat(cli.testnet.Dir)
   183  			if os.IsNotExist(err) {
   184  				err = Setup(cli.testnet, cli.infp)
   185  			}
   186  			if err != nil {
   187  				return err
   188  			}
   189  			return Start(cli.testnet)
   190  		},
   191  	})
   192  
   193  	cli.root.AddCommand(&cobra.Command{
   194  		Use:   "perturb",
   195  		Short: "Perturbs the Docker testnet, e.g. by restarting or disconnecting nodes",
   196  		RunE: func(cmd *cobra.Command, args []string) error {
   197  			return Perturb(cli.testnet)
   198  		},
   199  	})
   200  
   201  	cli.root.AddCommand(&cobra.Command{
   202  		Use:   "wait",
   203  		Short: "Waits for a few blocks to be produced and all nodes to catch up",
   204  		RunE: func(cmd *cobra.Command, args []string) error {
   205  			return Wait(cli.testnet, 5)
   206  		},
   207  	})
   208  
   209  	cli.root.AddCommand(&cobra.Command{
   210  		Use:   "stop",
   211  		Short: "Stops the Docker testnet",
   212  		RunE: func(cmd *cobra.Command, args []string) error {
   213  			logger.Info("Stopping testnet")
   214  			return execCompose(cli.testnet.Dir, "down")
   215  		},
   216  	})
   217  
   218  	cli.root.AddCommand(&cobra.Command{
   219  		Use:   "load",
   220  		Short: "Generates transaction load until the command is canceled",
   221  		RunE: func(cmd *cobra.Command, args []string) (err error) {
   222  			return Load(context.Background(), cli.testnet)
   223  		},
   224  	})
   225  
   226  	cli.root.AddCommand(&cobra.Command{
   227  		Use:   "evidence [amount]",
   228  		Args:  cobra.MaximumNArgs(1),
   229  		Short: "Generates and broadcasts evidence to a random node",
   230  		RunE: func(cmd *cobra.Command, args []string) (err error) {
   231  			amount := 1
   232  
   233  			if len(args) == 1 {
   234  				amount, err = strconv.Atoi(args[0])
   235  				if err != nil {
   236  					return err
   237  				}
   238  			}
   239  
   240  			return InjectEvidence(
   241  				cmd.Context(),
   242  				rand.New(rand.NewSource(randomSeed)), //nolint: gosec
   243  				cli.testnet,
   244  				amount,
   245  			)
   246  		},
   247  	})
   248  
   249  	cli.root.AddCommand(&cobra.Command{
   250  		Use:   "test",
   251  		Short: "Runs test cases against a running testnet",
   252  		RunE: func(cmd *cobra.Command, args []string) error {
   253  			return Test(cli.testnet)
   254  		},
   255  	})
   256  
   257  	cli.root.AddCommand(&cobra.Command{
   258  		Use:   "cleanup",
   259  		Short: "Removes the testnet directory",
   260  		RunE: func(cmd *cobra.Command, args []string) error {
   261  			return Cleanup(cli.testnet)
   262  		},
   263  	})
   264  
   265  	cli.root.AddCommand(&cobra.Command{
   266  		Use:   "logs",
   267  		Short: "Shows the testnet logs",
   268  		RunE: func(cmd *cobra.Command, args []string) error {
   269  			return execComposeVerbose(cli.testnet.Dir, "logs")
   270  		},
   271  	})
   272  
   273  	cli.root.AddCommand(&cobra.Command{
   274  		Use:   "tail",
   275  		Short: "Tails the testnet logs",
   276  		RunE: func(cmd *cobra.Command, args []string) error {
   277  			return execComposeVerbose(cli.testnet.Dir, "logs", "--follow")
   278  		},
   279  	})
   280  
   281  	cli.root.AddCommand(&cobra.Command{
   282  		Use:   "benchmark",
   283  		Short: "Benchmarks testnet",
   284  		Long: `Benchmarks the following metrics:
   285  	Mean Block Interval
   286  	Standard Deviation
   287  	Min Block Interval
   288  	Max Block Interval
   289  over a 100 block sampling period.
   290  
   291  Does not run any perturbations.
   292  		`,
   293  		RunE: func(cmd *cobra.Command, args []string) error {
   294  			if err := Cleanup(cli.testnet); err != nil {
   295  				return err
   296  			}
   297  			if err := Setup(cli.testnet, cli.infp); err != nil {
   298  				return err
   299  			}
   300  
   301  			chLoadResult := make(chan error)
   302  			ctx, loadCancel := context.WithCancel(context.Background())
   303  			defer loadCancel()
   304  			go func() {
   305  				err := Load(ctx, cli.testnet)
   306  				if err != nil {
   307  					logger.Error(fmt.Sprintf("Transaction load errored: %v", err.Error()))
   308  				}
   309  				chLoadResult <- err
   310  			}()
   311  
   312  			if err := Start(cli.testnet); err != nil {
   313  				return err
   314  			}
   315  
   316  			if err := Wait(cli.testnet, 5); err != nil { // allow some txs to go through
   317  				return err
   318  			}
   319  
   320  			// we benchmark performance over the next 100 blocks
   321  			if err := Benchmark(cli.testnet, 100); err != nil {
   322  				return err
   323  			}
   324  
   325  			loadCancel()
   326  			if err := <-chLoadResult; err != nil {
   327  				return err
   328  			}
   329  
   330  			if err := Cleanup(cli.testnet); err != nil {
   331  				return err
   332  			}
   333  
   334  			return nil
   335  		},
   336  	})
   337  
   338  	return cli
   339  }
   340  
   341  // Run runs the CLI.
   342  func (cli *CLI) Run() {
   343  	if err := cli.root.Execute(); err != nil {
   344  		logger.Error(err.Error())
   345  		os.Exit(1)
   346  	}
   347  }