github.com/badrootd/celestia-core@v0.0.0-20240305091328-aa4207a4b25d/test/e2e/runner/main.go (about)

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