github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/tools/e2e.go (about)

     1  // Package tools provides common tools and utilities for all unit and integration tests
     2  /*
     3   * Copyright (c) 2018-2023, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package tools
     6  
     7  import (
     8  	"bufio"
     9  	"fmt"
    10  	"io"
    11  	"math/rand"
    12  	"os"
    13  	"os/exec"
    14  	"regexp"
    15  	"sort"
    16  	"strings"
    17  	"sync"
    18  
    19  	"github.com/NVIDIA/aistore/api"
    20  	"github.com/NVIDIA/aistore/api/apc"
    21  	"github.com/NVIDIA/aistore/cmn"
    22  	"github.com/NVIDIA/aistore/cmn/cos"
    23  	"github.com/NVIDIA/aistore/cmn/k8s"
    24  	"github.com/NVIDIA/aistore/core/meta"
    25  	"github.com/NVIDIA/aistore/tools/tlog"
    26  	"github.com/NVIDIA/aistore/tools/trand"
    27  	"github.com/onsi/ginkgo/v2"
    28  	"github.com/onsi/gomega"
    29  )
    30  
    31  type E2EFramework struct {
    32  	Dir  string
    33  	Vars map[string]string // Custom variables passed to input and output files.
    34  }
    35  
    36  var onceK8s sync.Once
    37  
    38  func (f *E2EFramework) RunE2ETest(fileName string) {
    39  	var (
    40  		outs []string
    41  
    42  		lastResult = ""
    43  		bucket     = strings.ToLower(trand.String(10))
    44  		space      = regexp.MustCompile(`\s+`) // Used to replace all whitespace with single spaces.
    45  		target     = randomTarget()
    46  		mountpath  = randomMountpath(target)
    47  		backends   = retrieveBackendProviders()
    48  		etlName    = "etlname-" + strings.ToLower(trand.String(4))
    49  
    50  		inputFileName   = fileName + ".in"
    51  		outputFileName  = fileName + ".stdout"
    52  		cleanupFileName = fileName + ".cleanup"
    53  	)
    54  
    55  	// Create random file.
    56  	tmpFile, err := os.CreateTemp("", "e2e-")
    57  	gomega.Expect(err).NotTo(gomega.HaveOccurred())
    58  	object := tmpFile.Name()
    59  	tmpFile.Close()
    60  	defer os.RemoveAll(object)
    61  
    62  	substituteVariables := func(s string) string {
    63  		s = strings.ReplaceAll(s, "$BUCKET", bucket)
    64  		s = strings.ReplaceAll(s, "$OBJECT", object)
    65  		s = strings.ReplaceAll(s, "$RANDOM_TARGET", target.ID())
    66  		s = strings.ReplaceAll(s, "$RANDOM_MOUNTPATH", mountpath)
    67  		s = strings.ReplaceAll(s, "$DIR", f.Dir)
    68  		s = strings.ReplaceAll(s, "$RESULT", lastResult)
    69  		s = strings.ReplaceAll(s, "$BACKENDS", strings.Join(backends, ","))
    70  		s = strings.ReplaceAll(s, "$ETL_NAME", etlName)
    71  		for k, v := range f.Vars {
    72  			s = strings.ReplaceAll(s, "$"+k, v)
    73  		}
    74  		return s
    75  	}
    76  
    77  	defer func() {
    78  		if err := destroyMatchingBuckets(bucket); err != nil {
    79  			tlog.Logf("failed to remove buckets: %v", err)
    80  		}
    81  
    82  		fh, err := os.Open(cleanupFileName)
    83  		if err != nil {
    84  			return
    85  		}
    86  		defer fh.Close()
    87  		for _, line := range readContent(fh, true /*ignoreEmpty*/) {
    88  			scmd := substituteVariables(line)
    89  			_ = exec.Command("bash", "-c", scmd).Run()
    90  		}
    91  	}()
    92  
    93  	inFile, err := os.Open(inputFileName)
    94  	gomega.Expect(err).NotTo(gomega.HaveOccurred())
    95  	defer inFile.Close()
    96  
    97  	for _, scmd := range readContent(inFile, true /*ignoreEmpty*/) {
    98  		var (
    99  			saveResult    = false
   100  			ignoreOutput  = false
   101  			expectFail    = false
   102  			expectFailMsg = ""
   103  		)
   104  
   105  		// Parse comment if present.
   106  		if strings.Contains(scmd, " //") {
   107  			var comment string
   108  			tmp := strings.Split(scmd, " //")
   109  			scmd, comment = tmp[0], tmp[1]
   110  			if strings.Contains(comment, "SAVE_RESULT") {
   111  				saveResult = true
   112  			}
   113  			if strings.Contains(comment, "IGNORE") {
   114  				ignoreOutput = true
   115  			}
   116  			if strings.Contains(comment, "FAIL") {
   117  				expectFail = true
   118  				if strings.Count(comment, `"`) >= 2 {
   119  					firstIdx := strings.Index(comment, `"`)
   120  					lastIdx := strings.LastIndex(comment, `"`)
   121  					expectFailMsg = comment[firstIdx+1 : lastIdx]
   122  					expectFailMsg = substituteVariables(expectFailMsg)
   123  					if !isLineRegex(expectFailMsg) {
   124  						expectFailMsg = strings.ToLower(expectFailMsg)
   125  					}
   126  				}
   127  			}
   128  		} else if strings.HasPrefix(scmd, "// RUN") {
   129  			comment := strings.TrimSpace(strings.TrimPrefix(scmd, "// RUN"))
   130  
   131  			switch comment {
   132  			case "local-deployment":
   133  				// Skip running test if requires local deployment and the cluster
   134  				// is not in testing env.
   135  				config, err := getClusterConfig()
   136  				cos.AssertNoErr(err)
   137  				if !config.TestingEnv() {
   138  					ginkgo.Skip("requires local deployment")
   139  					return
   140  				}
   141  
   142  				continue
   143  			case "authn":
   144  				// Skip running AuthN e2e tests if the former is not enabled
   145  				// (compare w/ `SkipTestArgs.RequiresAuth`)
   146  				if config, err := getClusterConfig(); err == nil && config.Auth.Enabled {
   147  					continue
   148  				}
   149  				ginkgo.Skip("AuthN not enabled - skipping")
   150  				return
   151  			case "k8s":
   152  				onceK8s.Do(k8s.Init)
   153  				if k8s.IsK8s() {
   154  					continue
   155  				}
   156  				ginkgo.Skip("not running in K8s - skipping")
   157  				return
   158  			default:
   159  				cos.AssertMsg(false, "invalid run mode: "+comment)
   160  			}
   161  		} else if strings.HasPrefix(scmd, "// SKIP") {
   162  			message := strings.TrimSpace(strings.TrimPrefix(scmd, "// SKIP"))
   163  			message = strings.Trim(message, `"`)
   164  			ginkgo.Skip(message)
   165  			return
   166  		}
   167  
   168  		scmd = substituteVariables(scmd)
   169  		if strings.Contains(scmd, "$PRINT_SIZE") {
   170  			// Expecting: $PRINT_SIZE FILE_NAME
   171  			fileName := strings.ReplaceAll(scmd, "$PRINT_SIZE ", "")
   172  			scmd = fmt.Sprintf("wc -c %s | awk '{print $1}'", fileName)
   173  		}
   174  		cmd := exec.Command("bash", "-c", scmd)
   175  		b, err := cmd.Output()
   176  		if expectFail {
   177  			var desc string
   178  			if ee, ok := err.(*exec.ExitError); ok {
   179  				desc = strings.ToLower(string(ee.Stderr))
   180  			}
   181  			gomega.Expect(err).To(gomega.HaveOccurred(), "expected FAIL but command succeeded")
   182  			gomega.Expect(desc).To(gomega.ContainSubstring(expectFailMsg))
   183  			continue
   184  		}
   185  		var desc string
   186  		if ee, ok := err.(*exec.ExitError); ok {
   187  			desc = string(ee.Stderr)
   188  		}
   189  		desc = fmt.Sprintf("cmd: %q, err: %s", cmd.String(), desc)
   190  		gomega.Expect(err).NotTo(gomega.HaveOccurred(), desc)
   191  
   192  		if saveResult {
   193  			lastResult = strings.TrimSpace(string(b))
   194  		} else if !ignoreOutput {
   195  			out := strings.Split(string(b), "\n")
   196  			if out[len(out)-1] == "" {
   197  				out = out[:len(out)-1]
   198  			}
   199  			outs = append(outs, out...)
   200  		}
   201  	}
   202  
   203  	outFile, err := os.Open(outputFileName)
   204  	gomega.Expect(err).NotTo(gomega.HaveOccurred())
   205  	defer outFile.Close()
   206  
   207  	outLines := readContent(outFile, false /*ignoreEmpty*/)
   208  	for idx, line := range outLines {
   209  		gomega.Expect(idx).To(
   210  			gomega.BeNumerically("<", len(outs)),
   211  			"output file has more lines that were produced",
   212  		)
   213  		expectedOut := space.ReplaceAllString(line, "")
   214  		expectedOut = substituteVariables(expectedOut)
   215  
   216  		out := strings.TrimSpace(outs[idx])
   217  		out = space.ReplaceAllString(out, "")
   218  		// Sometimes quotation marks are returned which are not visible on
   219  		// console so we just remove them.
   220  		out = strings.ReplaceAll(out, "&#34;", "")
   221  		if isLineRegex(expectedOut) {
   222  			gomega.Expect(out).To(gomega.MatchRegexp(expectedOut))
   223  		} else {
   224  			gomega.Expect(out).To(gomega.Equal(expectedOut), "%s: %d", outputFileName, idx+1)
   225  		}
   226  	}
   227  
   228  	gomega.Expect(len(outLines)).To(
   229  		gomega.Equal(len(outs)),
   230  		"more lines were produced than were in output file",
   231  	)
   232  }
   233  
   234  //
   235  // helper methods
   236  //
   237  
   238  func destroyMatchingBuckets(subName string) (err error) {
   239  	proxyURL := GetPrimaryURL()
   240  	bp := BaseAPIParams(proxyURL)
   241  
   242  	bcks, err := api.ListBuckets(bp, cmn.QueryBcks{Provider: apc.AIS}, apc.FltExists)
   243  	if err != nil {
   244  		return err
   245  	}
   246  
   247  	for _, bck := range bcks {
   248  		if !strings.Contains(bck.Name, subName) {
   249  			continue
   250  		}
   251  		if errD := api.DestroyBucket(bp, bck); errD != nil && err == nil {
   252  			err = errD
   253  		}
   254  	}
   255  
   256  	return err
   257  }
   258  
   259  func randomTarget() *meta.Snode {
   260  	smap, err := api.GetClusterMap(BaseAPIParams(proxyURLReadOnly))
   261  	gomega.Expect(err).NotTo(gomega.HaveOccurred())
   262  	si, err := smap.GetRandTarget()
   263  	gomega.Expect(err).NotTo(gomega.HaveOccurred())
   264  	return si
   265  }
   266  
   267  func randomMountpath(target *meta.Snode) string {
   268  	mpaths, err := api.GetMountpaths(BaseAPIParams(proxyURLReadOnly), target)
   269  	gomega.Expect(err).NotTo(gomega.HaveOccurred())
   270  	gomega.Expect(len(mpaths.Available)).NotTo(gomega.Equal(0))
   271  	return mpaths.Available[rand.Intn(len(mpaths.Available))]
   272  }
   273  
   274  func retrieveBackendProviders() []string {
   275  	target := randomTarget()
   276  	config, err := api.GetDaemonConfig(BaseAPIParams(proxyURLReadOnly), target)
   277  	gomega.Expect(err).NotTo(gomega.HaveOccurred())
   278  	set := cos.NewStrSet()
   279  	for b := range config.Backend.Providers {
   280  		set.Set(b)
   281  	}
   282  	set.Set(apc.AIS)
   283  	backends := set.ToSlice()
   284  	sort.Strings(backends)
   285  	return backends
   286  }
   287  
   288  func readContent(r io.Reader, ignoreEmpty bool) []string {
   289  	var (
   290  		scanner = bufio.NewScanner(r)
   291  		lines   = make([]string, 0, 4)
   292  	)
   293  	for scanner.Scan() {
   294  		line := scanner.Text()
   295  		if line == "" && ignoreEmpty {
   296  			continue
   297  		}
   298  		lines = append(lines, line)
   299  	}
   300  	gomega.Expect(scanner.Err()).NotTo(gomega.HaveOccurred())
   301  	return lines
   302  }
   303  
   304  func isLineRegex(msg string) bool {
   305  	return len(msg) > 2 && msg[0] == '^' && msg[len(msg)-1] == '$'
   306  }