github.com/smartcontractkit/chainlink-testing-framework/libs@v0.0.0-20240227141906-ec710b4eb1a3/gauntlet/gauntlet.go (about)

     1  // Package gauntlet enables the framework to interface with the chainlink gauntlet project
     2  package gauntlet
     3  
     4  import (
     5  	"bufio"
     6  	"errors"
     7  	"fmt"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/avast/retry-go"
    15  	"github.com/google/uuid"
    16  	"github.com/rs/zerolog/log"
    17  )
    18  
    19  var (
    20  	execDir string
    21  )
    22  
    23  // Gauntlet contains helpful data to run gauntlet commands
    24  type Gauntlet struct {
    25  	exec          string
    26  	Command       string
    27  	Network       string
    28  	NetworkConfig map[string]string
    29  }
    30  
    31  // NewGauntlet Sets up a gauntlet struct and checks if the yarn executable exists.
    32  func NewGauntlet() (*Gauntlet, error) {
    33  	yarn, err := exec.LookPath("yarn")
    34  	if err != nil {
    35  		return &Gauntlet{}, errors.New("'yarn' is not installed")
    36  	}
    37  	log.Debug().Str("PATH", yarn).Msg("Executable Path")
    38  	os.Setenv("SKIP_PROMPTS", "true")
    39  	g := &Gauntlet{
    40  		exec:          yarn,
    41  		Command:       "gauntlet", // Setting gauntlet as the default command
    42  		NetworkConfig: make(map[string]string),
    43  	}
    44  	g.GenerateRandomNetwork()
    45  	return g, nil
    46  }
    47  
    48  // Flag returns a string formatted in the expected gauntlet's flag form
    49  func (g *Gauntlet) Flag(flag, value string) string {
    50  	return fmt.Sprintf("--%s=%s", flag, value)
    51  }
    52  
    53  func (g *Gauntlet) SetWorkingDir(wrkDir string) {
    54  	execDir = wrkDir
    55  }
    56  
    57  // GenerateRandomNetwork Creates and sets a random network prepended with test
    58  func (g *Gauntlet) GenerateRandomNetwork() {
    59  	r := uuid.NewString()[0:8]
    60  	t := time.Now().UnixMilli()
    61  	g.Network = fmt.Sprintf("test%v%s", t, r)
    62  	log.Debug().Str("Network", g.Network).Msg("Generated Network Name")
    63  }
    64  
    65  type ExecCommandOptions struct {
    66  	ErrHandling       []string
    67  	CheckErrorsInRead bool
    68  	RetryCount        int
    69  	RetryDelay        time.Duration
    70  }
    71  
    72  // ExecCommand Executes a gauntlet or yarn command with the provided arguments.
    73  //
    74  //	It will also check for any errors you specify in the output via the errHandling slice.
    75  func (g *Gauntlet) ExecCommand(args []string, options ExecCommandOptions) (string, error) {
    76  	output := ""
    77  	var updatedArgs []string
    78  	if g.Command == "gauntlet" {
    79  		updatedArgs = append([]string{g.Command}, args...)
    80  		// Appending network to the gauntlet command
    81  		updatedArgs = insertArg(updatedArgs, 2, g.Flag("network", g.Network))
    82  	} else {
    83  		updatedArgs = args
    84  	}
    85  
    86  	printArgs(updatedArgs)
    87  
    88  	cmd := exec.Command(g.exec, updatedArgs...) // #nosec G204
    89  	if execDir != "" {
    90  		cmd.Dir = execDir
    91  	}
    92  	stdout, _ := cmd.StdoutPipe()
    93  	stderr, _ := cmd.StderrPipe()
    94  	if err := cmd.Start(); err != nil {
    95  		return output, err
    96  	}
    97  
    98  	reader := bufio.NewReader(stdout)
    99  	line, err := reader.ReadString('\n')
   100  	for err == nil {
   101  		log.Info().Str("stdout", line).Msg(g.Command)
   102  		output = fmt.Sprintf("%s%s", output, line)
   103  		if options.CheckErrorsInRead {
   104  			rerr := checkForErrors(options.ErrHandling, output)
   105  			if rerr != nil {
   106  				return output, rerr
   107  			}
   108  		}
   109  		line, err = reader.ReadString('\n')
   110  	}
   111  
   112  	reader = bufio.NewReader(stderr)
   113  	line, err = reader.ReadString('\n')
   114  	for err == nil {
   115  		log.Info().Str("stderr", line).Msg(g.Command)
   116  		output = fmt.Sprintf("%s%s", output, line)
   117  		if options.CheckErrorsInRead {
   118  			rerr := checkForErrors(options.ErrHandling, output)
   119  			if rerr != nil {
   120  				return output, rerr
   121  			}
   122  		}
   123  		line, err = reader.ReadString('\n')
   124  	}
   125  
   126  	rerr := checkForErrors(options.ErrHandling, output)
   127  	if rerr != nil {
   128  		return output, rerr
   129  	}
   130  
   131  	if strings.Compare("EOF", err.Error()) > 0 {
   132  		return output, err
   133  	}
   134  
   135  	// catch any exit codes
   136  	err = cmd.Wait()
   137  
   138  	log.Debug().Str("Command", g.Command).Msg("command Completed")
   139  	return output, err
   140  }
   141  
   142  // ExecCommandWithRetries Some commands are safe to retry and in ci this can be even more so needed.
   143  func (g *Gauntlet) ExecCommandWithRetries(args []string, options ExecCommandOptions) (string, error) {
   144  	var output string
   145  	var err error
   146  	if options.RetryDelay == 0 {
   147  		// default to 5 seconds
   148  		options.RetryDelay = time.Second * 5
   149  	}
   150  	err = retry.Do(
   151  		func() error {
   152  			output, err = g.ExecCommand(args, options)
   153  			return err
   154  		},
   155  		retry.Delay(options.RetryDelay),
   156  		retry.MaxDelay(options.RetryDelay),
   157  		retry.Attempts(uint(options.RetryCount)),
   158  	)
   159  
   160  	return output, err
   161  }
   162  
   163  // WriteNetworkConfigMap write a network config file for gauntlet testing.
   164  func (g *Gauntlet) WriteNetworkConfigMap(networkDirPath string) error {
   165  	file := filepath.Join(networkDirPath, fmt.Sprintf(".env.%s", g.Network))
   166  	f, err := os.OpenFile(file, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	defer f.Close()
   171  	for k, v := range g.NetworkConfig {
   172  		log.Debug().Str(k, v).Msg("Gauntlet .env config value:")
   173  		_, err = f.WriteString(fmt.Sprintf("\n%s=%s", k, v))
   174  		if err != nil {
   175  			return err
   176  		}
   177  	}
   178  	return nil
   179  }
   180  
   181  // checkForErrors Loops through provided err slice to see if the error exists in the output.
   182  func checkForErrors(errHandling []string, line string) error {
   183  	for _, e := range errHandling {
   184  		if strings.Contains(line, e) {
   185  			log.Debug().Str("Error", line).Msg("Gauntlet Error Found")
   186  			return fmt.Errorf("found a gauntlet error")
   187  		}
   188  	}
   189  	return nil
   190  }
   191  
   192  // insertArg inserts an argument into the args slice
   193  func insertArg(args []string, index int, valueToInsert string) []string {
   194  	if len(args) <= index { // nil or empty slice or after last element
   195  		return append(args, valueToInsert)
   196  	}
   197  	args = append(args[:index+1], args[index:]...) // index < len(a)
   198  	args[index] = valueToInsert
   199  	return args
   200  }
   201  
   202  // printArgs prints all the gauntlet args being used in a call to gauntlet
   203  func printArgs(args []string) {
   204  	out := "yarn"
   205  	for _, arg := range args {
   206  		out = fmt.Sprintf("%s %s", out, arg)
   207  
   208  	}
   209  	log.Info().Str("Command", out).Msg("Gauntlet")
   210  }
   211  
   212  func (g *Gauntlet) AddNetworkConfigVar(k string, v string) {
   213  	g.NetworkConfig[k] = v
   214  }