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, """, "") 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 }